From 0418dc5cf9880a2a1cff1cde8d9d258012150e3a Mon Sep 17 00:00:00 2001 From: wehub-resource-sync Date: Mon, 13 Jul 2026 12:45:00 +0800 Subject: [PATCH] chore: import upstream snapshot with attribution --- .agents/skills | 1 + .claude/skills/harness-eval/SKILL.md | 193 + .../harness-eval/references/feature-matrix.md | 47 + .../harness-eval/references/test-patterns.md | 150 + .claude/skills/pr-merge/SKILL.md | 99 + .../pr-merge/references/merge-scenarios.md | 131 + .github/ISSUE_TEMPLATE/bug_report.yml | 41 + .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.yml | 33 + .github/PULL_REQUEST_TEMPLATE.md | 15 + .github/workflows/autopilot-pages.yml | 59 + .github/workflows/autopilot-run-next.yml | 53 + .github/workflows/autopilot-scan.yml | 53 + .github/workflows/ci.yml | 83 + .gitignore | 37 + CHANGELOG.md | 105 + CONTRIBUTING.md | 68 + LICENSE | 21 + README.md | 879 +++++ README.wehub.md | 7 + README.zh-CN.md | 417 ++ RELEASE_NOTES_v0.1.8.md | 66 + RELEASE_NOTES_v0.1.9.md | 32 + assets/architecture-comic.png | Bin 0 -> 1415458 bytes assets/cli-typing.gif | Bin 0 -> 17881 bytes assets/harness-equation.png | Bin 0 -> 1125387 bytes assets/landing.png | Bin 0 -> 19413 bytes assets/logo.png | Bin 0 -> 907399 bytes assets/ohmo.png | Bin 0 -> 1017767 bytes assets/scene-agentloop.png | Bin 0 -> 898144 bytes assets/scene-context.png | Bin 0 -> 895250 bytes assets/scene-governance.png | Bin 0 -> 919765 bytes assets/scene-swarm.png | Bin 0 -> 818503 bytes assets/scene-toolkit.png | Bin 0 -> 808912 bytes autopilot-dashboard/.gitignore | 2 + autopilot-dashboard/index.html | 15 + autopilot-dashboard/package-lock.json | 1859 +++++++++ autopilot-dashboard/package.json | 22 + autopilot-dashboard/public/snapshot.json | 59 + autopilot-dashboard/src/App.tsx | 290 ++ .../src/components/HeroBackground.tsx | 181 + .../src/components/PipelineAnimation.tsx | 223 ++ autopilot-dashboard/src/index.css | 546 +++ autopilot-dashboard/src/main.tsx | 10 + autopilot-dashboard/src/types.ts | 84 + autopilot-dashboard/src/vite-env.d.ts | 1 + autopilot-dashboard/tsconfig.app.json | 22 + autopilot-dashboard/tsconfig.json | 4 + autopilot-dashboard/vite.config.ts | 11 + docs/SHOWCASE.md | 80 + docs/autopilot/.nojekyll | 0 docs/autopilot/assets/index-CENkxP5l.css | 1 + docs/autopilot/assets/index-gQnrxco6.js | 49 + docs/autopilot/index.html | 16 + docs/autopilot/snapshot.json | 59 + frontend/terminal/package-lock.json | 1198 ++++++ frontend/terminal/package.json | 21 + frontend/terminal/src/App.tsx | 589 +++ frontend/terminal/src/clipboardImage.ts | 173 + .../terminal/src/components/CommandPicker.tsx | 35 + frontend/terminal/src/components/Composer.tsx | 32 + .../src/components/ConversationView.tsx | 190 + frontend/terminal/src/components/Footer.tsx | 17 + .../src/components/MarkdownText.test.tsx | 119 + .../terminal/src/components/MarkdownText.tsx | 315 ++ .../src/components/ModalHost.test.tsx | 87 + .../terminal/src/components/ModalHost.tsx | 253 ++ .../src/components/PromptInput.test.ts | 16 + .../src/components/PromptInput.test.tsx | 244 ++ .../terminal/src/components/PromptInput.tsx | 254 ++ .../terminal/src/components/SelectModal.tsx | 44 + .../terminal/src/components/SidePanel.tsx | 152 + frontend/terminal/src/components/Spinner.tsx | 47 + .../terminal/src/components/StatusBar.tsx | 119 + .../terminal/src/components/SwarmPanel.tsx | 127 + .../terminal/src/components/TodoPanel.tsx | 91 + .../src/components/ToolCallDisplay.tsx | 148 + .../src/components/TranscriptPane.tsx | 57 + .../terminal/src/components/WelcomeBanner.tsx | 47 + .../terminal/src/hooks/useBackendSession.ts | 462 +++ frontend/terminal/src/index.tsx | 81 + frontend/terminal/src/theme/ThemeContext.tsx | 40 + frontend/terminal/src/theme/builtinThemes.ts | 161 + frontend/terminal/src/types.ts | 98 + frontend/terminal/tsconfig.json | 13 + ohmo/__init__.py | 5 + ohmo/__main__.py | 8 + ohmo/cli.py | 710 ++++ ohmo/gateway/__init__.py | 2 + ohmo/gateway/bridge.py | 414 ++ ohmo/gateway/config.py | 41 + ohmo/gateway/group_tool.py | 158 + ohmo/gateway/models.py | 33 + ohmo/gateway/notify.py | 78 + ohmo/gateway/provider_commands.py | 139 + ohmo/gateway/router.py | 31 + ohmo/gateway/runtime.py | 1254 ++++++ ohmo/gateway/service.py | 450 +++ ohmo/group_registry.py | 95 + ohmo/memory.py | 219 ++ ohmo/prompts.py | 74 + ohmo/runtime.py | 218 ++ ohmo/session_storage.py | 202 + ohmo/workspace.py | 319 ++ pyproject.toml | 85 + scripts/e2e_smoke.py | 807 ++++ scripts/install.ps1 | 329 ++ scripts/install.sh | 387 ++ scripts/install_dev.sh | 181 + scripts/local_system_scenarios.py | 248 ++ scripts/migrate_memory.py | 14 + scripts/react_tui_e2e.py | 161 + scripts/sync_nanobot_channels.sh | 90 + scripts/test_cli_flags.py | 157 + scripts/test_docker_sandbox_e2e.py | 654 ++++ scripts/test_harness_features.py | 239 ++ scripts/test_headless_rendering.py | 166 + scripts/test_react_tui_redesign.py | 187 + scripts/test_real_skills_plugins.py | 346 ++ scripts/test_tui_interactions.py | 202 + src/__init__.py | 0 src/openharness/__init__.py | 0 src/openharness/__main__.py | 6 + src/openharness/api/__init__.py | 21 + src/openharness/api/client.py | 271 ++ src/openharness/api/codex_client.py | 407 ++ src/openharness/api/copilot_auth.py | 240 ++ src/openharness/api/copilot_client.py | 134 + src/openharness/api/errors.py | 19 + src/openharness/api/openai_client.py | 484 +++ src/openharness/api/provider.py | 186 + src/openharness/api/registry.py | 437 +++ src/openharness/api/usage.py | 17 + src/openharness/auth/__init__.py | 29 + src/openharness/auth/external.py | 610 +++ src/openharness/auth/flows.py | 189 + src/openharness/auth/manager.py | 482 +++ src/openharness/auth/storage.py | 269 ++ src/openharness/autopilot/__init__.py | 23 + src/openharness/autopilot/service.py | 2239 +++++++++++ src/openharness/autopilot/types.py | 91 + src/openharness/bridge/__init__.py | 20 + src/openharness/bridge/manager.py | 106 + src/openharness/bridge/session_runner.py | 46 + src/openharness/bridge/types.py | 37 + src/openharness/bridge/work_secret.py | 41 + src/openharness/channels/UPSTREAM | 6 + src/openharness/channels/__init__.py | 22 + src/openharness/channels/adapter.py | 131 + src/openharness/channels/bus/__init__.py | 6 + src/openharness/channels/bus/events.py | 38 + src/openharness/channels/bus/queue.py | 44 + src/openharness/channels/impl/__init__.py | 6 + src/openharness/channels/impl/base.py | 141 + src/openharness/channels/impl/dingtalk.py | 445 +++ src/openharness/channels/impl/discord.py | 311 ++ src/openharness/channels/impl/email.py | 410 ++ src/openharness/channels/impl/feishu.py | 1342 +++++++ src/openharness/channels/impl/manager.py | 257 ++ src/openharness/channels/impl/matrix.py | 700 ++++ src/openharness/channels/impl/mochat.py | 897 +++++ src/openharness/channels/impl/qq.py | 141 + src/openharness/channels/impl/slack.py | 284 ++ src/openharness/channels/impl/telegram.py | 525 +++ src/openharness/channels/impl/whatsapp.py | 159 + src/openharness/cli.py | 2551 +++++++++++++ src/openharness/commands/__init__.py | 21 + src/openharness/commands/registry.py | 2783 ++++++++++++++ src/openharness/config/__init__.py | 34 + src/openharness/config/paths.py | 160 + src/openharness/config/schema.py | 119 + src/openharness/config/settings.py | 1104 ++++++ src/openharness/coordinator/__init__.py | 12 + .../coordinator/agent_definitions.py | 975 +++++ .../coordinator/coordinator_mode.py | 520 +++ src/openharness/engine/__init__.py | 80 + src/openharness/engine/cost_tracker.py | 24 + src/openharness/engine/messages.py | 222 ++ src/openharness/engine/query.py | 1057 ++++++ src/openharness/engine/query_engine.py | 306 ++ src/openharness/engine/stream_events.py | 90 + src/openharness/hooks/__init__.py | 50 + src/openharness/hooks/events.py | 20 + src/openharness/hooks/executor.py | 242 ++ src/openharness/hooks/hot_reload.py | 31 + src/openharness/hooks/loader.py | 68 + src/openharness/hooks/schemas.py | 66 + src/openharness/hooks/types.py | 38 + src/openharness/keybindings/__init__.py | 14 + .../keybindings/default_bindings.py | 11 + src/openharness/keybindings/loader.py | 22 + src/openharness/keybindings/parser.py | 18 + src/openharness/keybindings/resolver.py | 13 + src/openharness/mcp/__init__.py | 79 + src/openharness/mcp/client.py | 298 ++ src/openharness/mcp/config.py | 16 + src/openharness/mcp/types.py | 76 + src/openharness/memory/__init__.py | 25 + src/openharness/memory/agent.py | 91 + src/openharness/memory/manager.py | 183 + src/openharness/memory/memdir.py | 52 + src/openharness/memory/migrate.py | 137 + src/openharness/memory/paths.py | 22 + src/openharness/memory/relevance.py | 145 + src/openharness/memory/scan.py | 116 + src/openharness/memory/schema.py | 443 +++ src/openharness/memory/search.py | 71 + src/openharness/memory/team.py | 90 + src/openharness/memory/types.py | 33 + src/openharness/memory/usage.py | 153 + src/openharness/output_styles/__init__.py | 5 + src/openharness/output_styles/loader.py | 42 + src/openharness/permissions/__init__.py | 26 + src/openharness/permissions/checker.py | 200 + src/openharness/permissions/modes.py | 13 + src/openharness/personalization/__init__.py | 6 + src/openharness/personalization/extractor.py | 138 + src/openharness/personalization/rules.py | 64 + .../personalization/session_hook.py | 65 + src/openharness/platforms.py | 86 + src/openharness/plugins/__init__.py | 53 + src/openharness/plugins/bundled/__init__.py | 0 src/openharness/plugins/installer.py | 39 + src/openharness/plugins/loader.py | 730 ++++ src/openharness/plugins/schemas.py | 24 + src/openharness/plugins/types.py | 59 + src/openharness/prompts/__init__.py | 14 + src/openharness/prompts/claudemd.py | 48 + src/openharness/prompts/context.py | 187 + src/openharness/prompts/environment.py | 135 + src/openharness/prompts/system_prompt.py | 109 + src/openharness/sandbox/Dockerfile | 6 + src/openharness/sandbox/__init__.py | 33 + src/openharness/sandbox/adapter.py | 148 + src/openharness/sandbox/docker_backend.py | 232 ++ src/openharness/sandbox/docker_image.py | 103 + src/openharness/sandbox/path_validator.py | 37 + src/openharness/sandbox/session.py | 63 + src/openharness/services/__init__.py | 30 + .../services/autodream/__init__.py | 34 + src/openharness/services/autodream/backup.py | 104 + src/openharness/services/autodream/lock.py | 138 + src/openharness/services/autodream/prompt.py | 128 + src/openharness/services/autodream/service.py | 313 ++ src/openharness/services/compact/__init__.py | 1725 +++++++++ src/openharness/services/cron.py | 137 + src/openharness/services/cron_scheduler.py | 595 +++ src/openharness/services/lsp/__init__.py | 216 ++ .../services/memory_extract/__init__.py | 261 ++ src/openharness/services/oauth/__init__.py | 0 src/openharness/services/session_backend.py | 97 + .../services/session_memory/__init__.py | 139 + src/openharness/services/session_storage.py | 230 ++ src/openharness/services/token_estimation.py | 15 + src/openharness/services/tool_outputs.py | 55 + src/openharness/skills/__init__.py | 44 + src/openharness/skills/_frontmatter.py | 97 + src/openharness/skills/bundled/__init__.py | 75 + .../skills/bundled/content/commit.md | 25 + .../skills/bundled/content/debug.md | 25 + .../skills/bundled/content/diagnose.md | 35 + .../skills/bundled/content/plan.md | 34 + .../skills/bundled/content/review.md | 26 + .../skills/bundled/content/simplify.md | 26 + .../skills/bundled/content/skill-creator.md | 175 + .../skills/bundled/content/test.md | 31 + src/openharness/skills/loader.py | 229 ++ src/openharness/skills/registry.py | 29 + src/openharness/skills/types.py | 24 + src/openharness/state/__init__.py | 6 + src/openharness/state/app_state.py | 30 + src/openharness/state/store.py | 40 + src/openharness/swarm/__init__.py | 70 + src/openharness/swarm/in_process.py | 693 ++++ src/openharness/swarm/lockfile.py | 24 + src/openharness/swarm/mailbox.py | 522 +++ src/openharness/swarm/permission_sync.py | 1168 ++++++ src/openharness/swarm/registry.py | 410 ++ src/openharness/swarm/spawn_utils.py | 229 ++ src/openharness/swarm/subprocess_backend.py | 171 + src/openharness/swarm/team_lifecycle.py | 910 +++++ src/openharness/swarm/types.py | 398 ++ src/openharness/swarm/worktree.py | 315 ++ src/openharness/tasks/__init__.py | 18 + src/openharness/tasks/local_agent_task.py | 28 + src/openharness/tasks/local_shell_task.py | 17 + src/openharness/tasks/manager.py | 475 +++ src/openharness/tasks/stop_task.py | 11 + src/openharness/tasks/types.py | 32 + src/openharness/themes/__init__.py | 21 + src/openharness/themes/builtin.py | 89 + src/openharness/themes/loader.py | 55 + src/openharness/themes/schema.py | 54 + src/openharness/tools/__init__.py | 107 + src/openharness/tools/agent_tool.py | 141 + .../tools/ask_user_question_tool.py | 46 + src/openharness/tools/base.py | 80 + src/openharness/tools/bash_tool.py | 218 ++ src/openharness/tools/brief_tool.py | 33 + src/openharness/tools/config_tool.py | 54 + src/openharness/tools/cron_create_tool.py | 105 + src/openharness/tools/cron_delete_tool.py | 32 + src/openharness/tools/cron_list_tool.py | 69 + src/openharness/tools/cron_toggle_tool.py | 37 + src/openharness/tools/enter_plan_mode_tool.py | 28 + src/openharness/tools/enter_worktree_tool.py | 80 + src/openharness/tools/exit_plan_mode_tool.py | 28 + src/openharness/tools/exit_worktree_tool.py | 42 + src/openharness/tools/file_edit_tool.py | 95 + src/openharness/tools/file_read_tool.py | 72 + src/openharness/tools/file_write_tool.py | 87 + src/openharness/tools/glob_tool.py | 175 + src/openharness/tools/grep_tool.py | 374 ++ .../tools/image_generation_tool.py | 356 ++ src/openharness/tools/image_to_text_tool.py | 237 ++ .../tools/list_mcp_resources_tool.py | 36 + src/openharness/tools/lsp_tool.py | 154 + src/openharness/tools/mcp_auth_tool.py | 71 + src/openharness/tools/mcp_tool.py | 72 + src/openharness/tools/notebook_edit_tool.py | 97 + .../tools/read_mcp_resource_tool.py | 38 + src/openharness/tools/remote_trigger_tool.py | 74 + src/openharness/tools/send_message_tool.py | 58 + src/openharness/tools/skill_tool.py | 43 + src/openharness/tools/sleep_tool.py | 32 + src/openharness/tools/task_create_tool.py | 56 + src/openharness/tools/task_get_tool.py | 33 + src/openharness/tools/task_list_tool.py | 35 + src/openharness/tools/task_output_tool.py | 35 + src/openharness/tools/task_stop_tool.py | 30 + src/openharness/tools/task_update_tool.py | 50 + src/openharness/tools/team_create_tool.py | 31 + src/openharness/tools/team_delete_tool.py | 30 + src/openharness/tools/todo_write_tool.py | 46 + src/openharness/tools/tool_search_tool.py | 38 + src/openharness/tools/web_fetch_tool.py | 117 + src/openharness/tools/web_search_tool.py | 119 + src/openharness/ui/__init__.py | 5 + src/openharness/ui/app.py | 320 ++ src/openharness/ui/backend_host.py | 941 +++++ src/openharness/ui/coordinator_drain.py | 198 + src/openharness/ui/input.py | 32 + src/openharness/ui/output.py | 265 ++ src/openharness/ui/permission_dialog.py | 14 + src/openharness/ui/protocol.py | 247 ++ src/openharness/ui/react_launcher.py | 179 + src/openharness/ui/runtime.py | 799 ++++ src/openharness/ui/textual_app.py | 495 +++ src/openharness/utils/__init__.py | 0 src/openharness/utils/file_lock.py | 86 + src/openharness/utils/fs.py | 98 + src/openharness/utils/helpers.py | 77 + src/openharness/utils/network_guard.py | 340 ++ src/openharness/utils/shell.py | 147 + src/openharness/vim/__init__.py | 5 + src/openharness/vim/transitions.py | 8 + src/openharness/voice/__init__.py | 7 + src/openharness/voice/keyterms.py | 10 + src/openharness/voice/stream_stt.py | 8 + src/openharness/voice/voice_mode.py | 44 + tests/__init__.py | 0 tests/conftest.py | 13 + tests/fixtures/fake_mcp_server.py | 21 + tests/test_api/__init__.py | 0 tests/test_api/test_client.py | 193 + tests/test_api/test_codex_client.py | 244 ++ tests/test_api/test_copilot_auth.py | 277 ++ tests/test_api/test_copilot_client.py | 140 + tests/test_api/test_openai_client.py | 499 +++ tests/test_auth/test_external.py | 653 ++++ tests/test_auth/test_flows.py | 124 + tests/test_autopilot/__init__.py | 0 tests/test_autopilot/test_verification.py | 234 ++ tests/test_bridge/test_core.py | 61 + tests/test_bridge/test_session_flow.py | 32 + tests/test_channels/test_base.py | 27 + tests/test_channels/test_feishu_security.py | 488 +++ tests/test_channels/test_telegram_security.py | 64 + tests/test_commands/__init__.py | 0 tests/test_commands/test_cli.py | 559 +++ tests/test_commands/test_command_flows.py | 172 + tests/test_commands/test_registry.py | 1420 +++++++ tests/test_config/__init__.py | 0 .../test_config/test_output_styles_loader.py | 26 + tests/test_config/test_paths.py | 69 + tests/test_config/test_schema.py | 42 + tests/test_config/test_settings.py | 914 +++++ .../test_agent_definitions.py | 222 ++ .../test_coordinator/test_coordinator_mode.py | 199 + tests/test_coordinator/test_registry.py | 26 + tests/test_engine/__init__.py | 0 tests/test_engine/test_messages.py | 61 + tests/test_engine/test_query_engine.py | 1551 ++++++++ tests/test_entrypoints/test_config_cli.py | 30 + .../test_no_stdlib_types_shadowing.py | 16 + tests/test_hooks/__init__.py | 0 tests/test_hooks/test_executor.py | 121 + tests/test_hooks/test_priority.py | 99 + tests/test_hooks_skills_plugins_real.py | 636 ++++ tests/test_install/test_windows_alias.py | 41 + tests/test_logging/test_format_strings.py | 51 + tests/test_mcp/__init__.py | 0 tests/test_mcp/test_client_errors.py | 231 ++ tests/test_mcp/test_http_flow.py | 90 + tests/test_mcp/test_integration.py | 79 + tests/test_mcp/test_stdio_flow.py | 54 + tests/test_memory/__init__.py | 0 .../test_memory/test_claude_runtime_memory.py | 223 ++ tests/test_memory/test_memdir.py | 302 ++ tests/test_merged_prs_on_autoagent.py | 665 ++++ tests/test_ohmo/test_cli.py | 262 ++ tests/test_ohmo/test_gateway.py | 3356 +++++++++++++++++ tests/test_ohmo/test_loading.py | 160 + tests/test_ohmo/test_ohmo_session_storage.py | 89 + tests/test_ohmo/test_prompts.py | 73 + tests/test_ohmo/test_workspace.py | 38 + tests/test_permissions/__init__.py | 0 tests/test_permissions/test_checker.py | 230 ++ tests/test_personalization/__init__.py | 0 tests/test_personalization/test_extractor.py | 94 + tests/test_platforms.py | 29 + tests/test_plugins/__init__.py | 0 tests/test_plugins/test_lifecycle_flow.py | 109 + tests/test_plugins/test_loader.py | 221 ++ tests/test_prompts/__init__.py | 0 tests/test_prompts/test_claudemd.py | 164 + tests/test_prompts/test_environment.py | 111 + tests/test_prompts/test_system_prompt.py | 68 + tests/test_real_large_tasks.py | 797 ++++ tests/test_sandbox/test_adapter.py | 132 + tests/test_sandbox/test_docker_backend.py | 293 ++ tests/test_sandbox/test_docker_image.py | 92 + tests/test_sandbox/test_path_validator.py | 89 + tests/test_services/test_autodream.py | 194 + tests/test_services/test_autopilot.py | 619 +++ tests/test_services/test_compact.py | 637 ++++ tests/test_services/test_cron.py | 179 + tests/test_services/test_cron_scheduler.py | 213 ++ tests/test_services/test_session_storage.py | 93 + tests/test_skills/test_loader.py | 376 ++ tests/test_swarm/__init__.py | 0 tests/test_swarm/test_imports.py | 21 + tests/test_swarm/test_in_process.py | 182 + tests/test_swarm/test_lockfile.py | 83 + tests/test_swarm/test_mailbox.py | 196 + tests/test_swarm/test_permission_sync.py | 182 + tests/test_swarm/test_registry.py | 114 + tests/test_swarm/test_spawn_utils.py | 102 + tests/test_swarm/test_subprocess_backend.py | 282 ++ tests/test_swarm/test_team_lifecycle.py | 197 + tests/test_swarm/test_types.py | 154 + tests/test_swarm/test_worktree.py | 130 + tests/test_tasks/test_manager.py | 233 ++ tests/test_tools/__init__.py | 0 tests/test_tools/test_bash_tool.py | 215 ++ tests/test_tools/test_core_tools.py | 425 +++ tests/test_tools/test_grep_tool.py | 193 + .../test_tools/test_image_generation_tool.py | 215 ++ tests/test_tools/test_image_to_text_tool.py | 278 ++ tests/test_tools/test_integration_flows.py | 299 ++ tests/test_tools/test_mcp_auth_tool.py | 101 + tests/test_tools/test_mcp_tool.py | 94 + tests/test_tools/test_task_tools.py | 248 ++ tests/test_tools/test_web_fetch_tool.py | 193 + tests/test_ui/__init__.py | 0 tests/test_ui/test_coordinator_drain.py | 183 + tests/test_ui/test_modes.py | 25 + tests/test_ui/test_project_plugin_security.py | 61 + tests/test_ui/test_react_backend.py | 891 +++++ tests/test_ui/test_react_launcher.py | 308 ++ tests/test_ui/test_runtime_api_key.py | 49 + tests/test_ui/test_runtime_close.py | 32 + tests/test_ui/test_runtime_plan_mode.py | 49 + tests/test_ui/test_runtime_plugin_tools.py | 67 + tests/test_ui/test_textual_app.py | 184 + tests/test_ui/test_tui_exit_sequence.py | 49 + tests/test_untested_features.py | 841 +++++ tests/test_utils/test_fs.py | 175 + tests/test_utils/test_helpers.py | 35 + tests/test_utils/test_network_guard.py | 154 + tests/test_utils/test_shell.py | 192 + 481 files changed, 93793 insertions(+) create mode 120000 .agents/skills create mode 100644 .claude/skills/harness-eval/SKILL.md create mode 100644 .claude/skills/harness-eval/references/feature-matrix.md create mode 100644 .claude/skills/harness-eval/references/test-patterns.md create mode 100644 .claude/skills/pr-merge/SKILL.md create mode 100644 .claude/skills/pr-merge/references/merge-scenarios.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/autopilot-pages.yml create mode 100644 .github/workflows/autopilot-run-next.yml create mode 100644 .github/workflows/autopilot-scan.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README.wehub.md create mode 100644 README.zh-CN.md create mode 100644 RELEASE_NOTES_v0.1.8.md create mode 100644 RELEASE_NOTES_v0.1.9.md create mode 100644 assets/architecture-comic.png create mode 100644 assets/cli-typing.gif create mode 100644 assets/harness-equation.png create mode 100644 assets/landing.png create mode 100644 assets/logo.png create mode 100644 assets/ohmo.png create mode 100644 assets/scene-agentloop.png create mode 100644 assets/scene-context.png create mode 100644 assets/scene-governance.png create mode 100644 assets/scene-swarm.png create mode 100644 assets/scene-toolkit.png create mode 100644 autopilot-dashboard/.gitignore create mode 100644 autopilot-dashboard/index.html create mode 100644 autopilot-dashboard/package-lock.json create mode 100644 autopilot-dashboard/package.json create mode 100644 autopilot-dashboard/public/snapshot.json create mode 100644 autopilot-dashboard/src/App.tsx create mode 100644 autopilot-dashboard/src/components/HeroBackground.tsx create mode 100644 autopilot-dashboard/src/components/PipelineAnimation.tsx create mode 100644 autopilot-dashboard/src/index.css create mode 100644 autopilot-dashboard/src/main.tsx create mode 100644 autopilot-dashboard/src/types.ts create mode 100644 autopilot-dashboard/src/vite-env.d.ts create mode 100644 autopilot-dashboard/tsconfig.app.json create mode 100644 autopilot-dashboard/tsconfig.json create mode 100644 autopilot-dashboard/vite.config.ts create mode 100644 docs/SHOWCASE.md create mode 100644 docs/autopilot/.nojekyll create mode 100644 docs/autopilot/assets/index-CENkxP5l.css create mode 100644 docs/autopilot/assets/index-gQnrxco6.js create mode 100644 docs/autopilot/index.html create mode 100644 docs/autopilot/snapshot.json create mode 100644 frontend/terminal/package-lock.json create mode 100644 frontend/terminal/package.json create mode 100644 frontend/terminal/src/App.tsx create mode 100644 frontend/terminal/src/clipboardImage.ts create mode 100644 frontend/terminal/src/components/CommandPicker.tsx create mode 100644 frontend/terminal/src/components/Composer.tsx create mode 100644 frontend/terminal/src/components/ConversationView.tsx create mode 100644 frontend/terminal/src/components/Footer.tsx create mode 100644 frontend/terminal/src/components/MarkdownText.test.tsx create mode 100644 frontend/terminal/src/components/MarkdownText.tsx create mode 100644 frontend/terminal/src/components/ModalHost.test.tsx create mode 100644 frontend/terminal/src/components/ModalHost.tsx create mode 100644 frontend/terminal/src/components/PromptInput.test.ts create mode 100644 frontend/terminal/src/components/PromptInput.test.tsx create mode 100644 frontend/terminal/src/components/PromptInput.tsx create mode 100644 frontend/terminal/src/components/SelectModal.tsx create mode 100644 frontend/terminal/src/components/SidePanel.tsx create mode 100644 frontend/terminal/src/components/Spinner.tsx create mode 100644 frontend/terminal/src/components/StatusBar.tsx create mode 100644 frontend/terminal/src/components/SwarmPanel.tsx create mode 100644 frontend/terminal/src/components/TodoPanel.tsx create mode 100644 frontend/terminal/src/components/ToolCallDisplay.tsx create mode 100644 frontend/terminal/src/components/TranscriptPane.tsx create mode 100644 frontend/terminal/src/components/WelcomeBanner.tsx create mode 100644 frontend/terminal/src/hooks/useBackendSession.ts create mode 100644 frontend/terminal/src/index.tsx create mode 100644 frontend/terminal/src/theme/ThemeContext.tsx create mode 100644 frontend/terminal/src/theme/builtinThemes.ts create mode 100644 frontend/terminal/src/types.ts create mode 100644 frontend/terminal/tsconfig.json create mode 100644 ohmo/__init__.py create mode 100644 ohmo/__main__.py create mode 100644 ohmo/cli.py create mode 100644 ohmo/gateway/__init__.py create mode 100644 ohmo/gateway/bridge.py create mode 100644 ohmo/gateway/config.py create mode 100644 ohmo/gateway/group_tool.py create mode 100644 ohmo/gateway/models.py create mode 100644 ohmo/gateway/notify.py create mode 100644 ohmo/gateway/provider_commands.py create mode 100644 ohmo/gateway/router.py create mode 100644 ohmo/gateway/runtime.py create mode 100644 ohmo/gateway/service.py create mode 100644 ohmo/group_registry.py create mode 100644 ohmo/memory.py create mode 100644 ohmo/prompts.py create mode 100644 ohmo/runtime.py create mode 100644 ohmo/session_storage.py create mode 100644 ohmo/workspace.py create mode 100644 pyproject.toml create mode 100644 scripts/e2e_smoke.py create mode 100644 scripts/install.ps1 create mode 100755 scripts/install.sh create mode 100755 scripts/install_dev.sh create mode 100644 scripts/local_system_scenarios.py create mode 100644 scripts/migrate_memory.py create mode 100644 scripts/react_tui_e2e.py create mode 100755 scripts/sync_nanobot_channels.sh create mode 100644 scripts/test_cli_flags.py create mode 100644 scripts/test_docker_sandbox_e2e.py create mode 100644 scripts/test_harness_features.py create mode 100644 scripts/test_headless_rendering.py create mode 100644 scripts/test_react_tui_redesign.py create mode 100644 scripts/test_real_skills_plugins.py create mode 100644 scripts/test_tui_interactions.py create mode 100644 src/__init__.py create mode 100644 src/openharness/__init__.py create mode 100644 src/openharness/__main__.py create mode 100644 src/openharness/api/__init__.py create mode 100644 src/openharness/api/client.py create mode 100644 src/openharness/api/codex_client.py create mode 100644 src/openharness/api/copilot_auth.py create mode 100644 src/openharness/api/copilot_client.py create mode 100644 src/openharness/api/errors.py create mode 100644 src/openharness/api/openai_client.py create mode 100644 src/openharness/api/provider.py create mode 100644 src/openharness/api/registry.py create mode 100644 src/openharness/api/usage.py create mode 100644 src/openharness/auth/__init__.py create mode 100644 src/openharness/auth/external.py create mode 100644 src/openharness/auth/flows.py create mode 100644 src/openharness/auth/manager.py create mode 100644 src/openharness/auth/storage.py create mode 100644 src/openharness/autopilot/__init__.py create mode 100644 src/openharness/autopilot/service.py create mode 100644 src/openharness/autopilot/types.py create mode 100644 src/openharness/bridge/__init__.py create mode 100644 src/openharness/bridge/manager.py create mode 100644 src/openharness/bridge/session_runner.py create mode 100644 src/openharness/bridge/types.py create mode 100644 src/openharness/bridge/work_secret.py create mode 100644 src/openharness/channels/UPSTREAM create mode 100644 src/openharness/channels/__init__.py create mode 100644 src/openharness/channels/adapter.py create mode 100644 src/openharness/channels/bus/__init__.py create mode 100644 src/openharness/channels/bus/events.py create mode 100644 src/openharness/channels/bus/queue.py create mode 100644 src/openharness/channels/impl/__init__.py create mode 100644 src/openharness/channels/impl/base.py create mode 100644 src/openharness/channels/impl/dingtalk.py create mode 100644 src/openharness/channels/impl/discord.py create mode 100644 src/openharness/channels/impl/email.py create mode 100644 src/openharness/channels/impl/feishu.py create mode 100644 src/openharness/channels/impl/manager.py create mode 100644 src/openharness/channels/impl/matrix.py create mode 100644 src/openharness/channels/impl/mochat.py create mode 100644 src/openharness/channels/impl/qq.py create mode 100644 src/openharness/channels/impl/slack.py create mode 100644 src/openharness/channels/impl/telegram.py create mode 100644 src/openharness/channels/impl/whatsapp.py create mode 100644 src/openharness/cli.py create mode 100644 src/openharness/commands/__init__.py create mode 100644 src/openharness/commands/registry.py create mode 100644 src/openharness/config/__init__.py create mode 100644 src/openharness/config/paths.py create mode 100644 src/openharness/config/schema.py create mode 100644 src/openharness/config/settings.py create mode 100644 src/openharness/coordinator/__init__.py create mode 100644 src/openharness/coordinator/agent_definitions.py create mode 100644 src/openharness/coordinator/coordinator_mode.py create mode 100644 src/openharness/engine/__init__.py create mode 100644 src/openharness/engine/cost_tracker.py create mode 100644 src/openharness/engine/messages.py create mode 100644 src/openharness/engine/query.py create mode 100644 src/openharness/engine/query_engine.py create mode 100644 src/openharness/engine/stream_events.py create mode 100644 src/openharness/hooks/__init__.py create mode 100644 src/openharness/hooks/events.py create mode 100644 src/openharness/hooks/executor.py create mode 100644 src/openharness/hooks/hot_reload.py create mode 100644 src/openharness/hooks/loader.py create mode 100644 src/openharness/hooks/schemas.py create mode 100644 src/openharness/hooks/types.py create mode 100644 src/openharness/keybindings/__init__.py create mode 100644 src/openharness/keybindings/default_bindings.py create mode 100644 src/openharness/keybindings/loader.py create mode 100644 src/openharness/keybindings/parser.py create mode 100644 src/openharness/keybindings/resolver.py create mode 100644 src/openharness/mcp/__init__.py create mode 100644 src/openharness/mcp/client.py create mode 100644 src/openharness/mcp/config.py create mode 100644 src/openharness/mcp/types.py create mode 100644 src/openharness/memory/__init__.py create mode 100644 src/openharness/memory/agent.py create mode 100644 src/openharness/memory/manager.py create mode 100644 src/openharness/memory/memdir.py create mode 100644 src/openharness/memory/migrate.py create mode 100644 src/openharness/memory/paths.py create mode 100644 src/openharness/memory/relevance.py create mode 100644 src/openharness/memory/scan.py create mode 100644 src/openharness/memory/schema.py create mode 100644 src/openharness/memory/search.py create mode 100644 src/openharness/memory/team.py create mode 100644 src/openharness/memory/types.py create mode 100644 src/openharness/memory/usage.py create mode 100644 src/openharness/output_styles/__init__.py create mode 100644 src/openharness/output_styles/loader.py create mode 100644 src/openharness/permissions/__init__.py create mode 100644 src/openharness/permissions/checker.py create mode 100644 src/openharness/permissions/modes.py create mode 100644 src/openharness/personalization/__init__.py create mode 100644 src/openharness/personalization/extractor.py create mode 100644 src/openharness/personalization/rules.py create mode 100644 src/openharness/personalization/session_hook.py create mode 100644 src/openharness/platforms.py create mode 100644 src/openharness/plugins/__init__.py create mode 100644 src/openharness/plugins/bundled/__init__.py create mode 100644 src/openharness/plugins/installer.py create mode 100644 src/openharness/plugins/loader.py create mode 100644 src/openharness/plugins/schemas.py create mode 100644 src/openharness/plugins/types.py create mode 100644 src/openharness/prompts/__init__.py create mode 100644 src/openharness/prompts/claudemd.py create mode 100644 src/openharness/prompts/context.py create mode 100644 src/openharness/prompts/environment.py create mode 100644 src/openharness/prompts/system_prompt.py create mode 100644 src/openharness/sandbox/Dockerfile create mode 100644 src/openharness/sandbox/__init__.py create mode 100644 src/openharness/sandbox/adapter.py create mode 100644 src/openharness/sandbox/docker_backend.py create mode 100644 src/openharness/sandbox/docker_image.py create mode 100644 src/openharness/sandbox/path_validator.py create mode 100644 src/openharness/sandbox/session.py create mode 100644 src/openharness/services/__init__.py create mode 100644 src/openharness/services/autodream/__init__.py create mode 100644 src/openharness/services/autodream/backup.py create mode 100644 src/openharness/services/autodream/lock.py create mode 100644 src/openharness/services/autodream/prompt.py create mode 100644 src/openharness/services/autodream/service.py create mode 100644 src/openharness/services/compact/__init__.py create mode 100644 src/openharness/services/cron.py create mode 100644 src/openharness/services/cron_scheduler.py create mode 100644 src/openharness/services/lsp/__init__.py create mode 100644 src/openharness/services/memory_extract/__init__.py create mode 100644 src/openharness/services/oauth/__init__.py create mode 100644 src/openharness/services/session_backend.py create mode 100644 src/openharness/services/session_memory/__init__.py create mode 100644 src/openharness/services/session_storage.py create mode 100644 src/openharness/services/token_estimation.py create mode 100644 src/openharness/services/tool_outputs.py create mode 100644 src/openharness/skills/__init__.py create mode 100644 src/openharness/skills/_frontmatter.py create mode 100644 src/openharness/skills/bundled/__init__.py create mode 100644 src/openharness/skills/bundled/content/commit.md create mode 100644 src/openharness/skills/bundled/content/debug.md create mode 100644 src/openharness/skills/bundled/content/diagnose.md create mode 100644 src/openharness/skills/bundled/content/plan.md create mode 100644 src/openharness/skills/bundled/content/review.md create mode 100644 src/openharness/skills/bundled/content/simplify.md create mode 100644 src/openharness/skills/bundled/content/skill-creator.md create mode 100644 src/openharness/skills/bundled/content/test.md create mode 100644 src/openharness/skills/loader.py create mode 100644 src/openharness/skills/registry.py create mode 100644 src/openharness/skills/types.py create mode 100644 src/openharness/state/__init__.py create mode 100644 src/openharness/state/app_state.py create mode 100644 src/openharness/state/store.py create mode 100644 src/openharness/swarm/__init__.py create mode 100644 src/openharness/swarm/in_process.py create mode 100644 src/openharness/swarm/lockfile.py create mode 100644 src/openharness/swarm/mailbox.py create mode 100644 src/openharness/swarm/permission_sync.py create mode 100644 src/openharness/swarm/registry.py create mode 100644 src/openharness/swarm/spawn_utils.py create mode 100644 src/openharness/swarm/subprocess_backend.py create mode 100644 src/openharness/swarm/team_lifecycle.py create mode 100644 src/openharness/swarm/types.py create mode 100644 src/openharness/swarm/worktree.py create mode 100644 src/openharness/tasks/__init__.py create mode 100644 src/openharness/tasks/local_agent_task.py create mode 100644 src/openharness/tasks/local_shell_task.py create mode 100644 src/openharness/tasks/manager.py create mode 100644 src/openharness/tasks/stop_task.py create mode 100644 src/openharness/tasks/types.py create mode 100644 src/openharness/themes/__init__.py create mode 100644 src/openharness/themes/builtin.py create mode 100644 src/openharness/themes/loader.py create mode 100644 src/openharness/themes/schema.py create mode 100644 src/openharness/tools/__init__.py create mode 100644 src/openharness/tools/agent_tool.py create mode 100644 src/openharness/tools/ask_user_question_tool.py create mode 100644 src/openharness/tools/base.py create mode 100644 src/openharness/tools/bash_tool.py create mode 100644 src/openharness/tools/brief_tool.py create mode 100644 src/openharness/tools/config_tool.py create mode 100644 src/openharness/tools/cron_create_tool.py create mode 100644 src/openharness/tools/cron_delete_tool.py create mode 100644 src/openharness/tools/cron_list_tool.py create mode 100644 src/openharness/tools/cron_toggle_tool.py create mode 100644 src/openharness/tools/enter_plan_mode_tool.py create mode 100644 src/openharness/tools/enter_worktree_tool.py create mode 100644 src/openharness/tools/exit_plan_mode_tool.py create mode 100644 src/openharness/tools/exit_worktree_tool.py create mode 100644 src/openharness/tools/file_edit_tool.py create mode 100644 src/openharness/tools/file_read_tool.py create mode 100644 src/openharness/tools/file_write_tool.py create mode 100644 src/openharness/tools/glob_tool.py create mode 100644 src/openharness/tools/grep_tool.py create mode 100644 src/openharness/tools/image_generation_tool.py create mode 100644 src/openharness/tools/image_to_text_tool.py create mode 100644 src/openharness/tools/list_mcp_resources_tool.py create mode 100644 src/openharness/tools/lsp_tool.py create mode 100644 src/openharness/tools/mcp_auth_tool.py create mode 100644 src/openharness/tools/mcp_tool.py create mode 100644 src/openharness/tools/notebook_edit_tool.py create mode 100644 src/openharness/tools/read_mcp_resource_tool.py create mode 100644 src/openharness/tools/remote_trigger_tool.py create mode 100644 src/openharness/tools/send_message_tool.py create mode 100644 src/openharness/tools/skill_tool.py create mode 100644 src/openharness/tools/sleep_tool.py create mode 100644 src/openharness/tools/task_create_tool.py create mode 100644 src/openharness/tools/task_get_tool.py create mode 100644 src/openharness/tools/task_list_tool.py create mode 100644 src/openharness/tools/task_output_tool.py create mode 100644 src/openharness/tools/task_stop_tool.py create mode 100644 src/openharness/tools/task_update_tool.py create mode 100644 src/openharness/tools/team_create_tool.py create mode 100644 src/openharness/tools/team_delete_tool.py create mode 100644 src/openharness/tools/todo_write_tool.py create mode 100644 src/openharness/tools/tool_search_tool.py create mode 100644 src/openharness/tools/web_fetch_tool.py create mode 100644 src/openharness/tools/web_search_tool.py create mode 100644 src/openharness/ui/__init__.py create mode 100644 src/openharness/ui/app.py create mode 100644 src/openharness/ui/backend_host.py create mode 100644 src/openharness/ui/coordinator_drain.py create mode 100644 src/openharness/ui/input.py create mode 100644 src/openharness/ui/output.py create mode 100644 src/openharness/ui/permission_dialog.py create mode 100644 src/openharness/ui/protocol.py create mode 100644 src/openharness/ui/react_launcher.py create mode 100644 src/openharness/ui/runtime.py create mode 100644 src/openharness/ui/textual_app.py create mode 100644 src/openharness/utils/__init__.py create mode 100644 src/openharness/utils/file_lock.py create mode 100644 src/openharness/utils/fs.py create mode 100644 src/openharness/utils/helpers.py create mode 100644 src/openharness/utils/network_guard.py create mode 100644 src/openharness/utils/shell.py create mode 100644 src/openharness/vim/__init__.py create mode 100644 src/openharness/vim/transitions.py create mode 100644 src/openharness/voice/__init__.py create mode 100644 src/openharness/voice/keyterms.py create mode 100644 src/openharness/voice/stream_stt.py create mode 100644 src/openharness/voice/voice_mode.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/fixtures/fake_mcp_server.py create mode 100644 tests/test_api/__init__.py create mode 100644 tests/test_api/test_client.py create mode 100644 tests/test_api/test_codex_client.py create mode 100644 tests/test_api/test_copilot_auth.py create mode 100644 tests/test_api/test_copilot_client.py create mode 100644 tests/test_api/test_openai_client.py create mode 100644 tests/test_auth/test_external.py create mode 100644 tests/test_auth/test_flows.py create mode 100644 tests/test_autopilot/__init__.py create mode 100644 tests/test_autopilot/test_verification.py create mode 100644 tests/test_bridge/test_core.py create mode 100644 tests/test_bridge/test_session_flow.py create mode 100644 tests/test_channels/test_base.py create mode 100644 tests/test_channels/test_feishu_security.py create mode 100644 tests/test_channels/test_telegram_security.py create mode 100644 tests/test_commands/__init__.py create mode 100644 tests/test_commands/test_cli.py create mode 100644 tests/test_commands/test_command_flows.py create mode 100644 tests/test_commands/test_registry.py create mode 100644 tests/test_config/__init__.py create mode 100644 tests/test_config/test_output_styles_loader.py create mode 100644 tests/test_config/test_paths.py create mode 100644 tests/test_config/test_schema.py create mode 100644 tests/test_config/test_settings.py create mode 100644 tests/test_coordinator/test_agent_definitions.py create mode 100644 tests/test_coordinator/test_coordinator_mode.py create mode 100644 tests/test_coordinator/test_registry.py create mode 100644 tests/test_engine/__init__.py create mode 100644 tests/test_engine/test_messages.py create mode 100644 tests/test_engine/test_query_engine.py create mode 100644 tests/test_entrypoints/test_config_cli.py create mode 100644 tests/test_entrypoints/test_no_stdlib_types_shadowing.py create mode 100644 tests/test_hooks/__init__.py create mode 100644 tests/test_hooks/test_executor.py create mode 100644 tests/test_hooks/test_priority.py create mode 100644 tests/test_hooks_skills_plugins_real.py create mode 100644 tests/test_install/test_windows_alias.py create mode 100644 tests/test_logging/test_format_strings.py create mode 100644 tests/test_mcp/__init__.py create mode 100644 tests/test_mcp/test_client_errors.py create mode 100644 tests/test_mcp/test_http_flow.py create mode 100644 tests/test_mcp/test_integration.py create mode 100644 tests/test_mcp/test_stdio_flow.py create mode 100644 tests/test_memory/__init__.py create mode 100644 tests/test_memory/test_claude_runtime_memory.py create mode 100644 tests/test_memory/test_memdir.py create mode 100644 tests/test_merged_prs_on_autoagent.py create mode 100644 tests/test_ohmo/test_cli.py create mode 100644 tests/test_ohmo/test_gateway.py create mode 100644 tests/test_ohmo/test_loading.py create mode 100644 tests/test_ohmo/test_ohmo_session_storage.py create mode 100644 tests/test_ohmo/test_prompts.py create mode 100644 tests/test_ohmo/test_workspace.py create mode 100644 tests/test_permissions/__init__.py create mode 100644 tests/test_permissions/test_checker.py create mode 100644 tests/test_personalization/__init__.py create mode 100644 tests/test_personalization/test_extractor.py create mode 100644 tests/test_platforms.py create mode 100644 tests/test_plugins/__init__.py create mode 100644 tests/test_plugins/test_lifecycle_flow.py create mode 100644 tests/test_plugins/test_loader.py create mode 100644 tests/test_prompts/__init__.py create mode 100644 tests/test_prompts/test_claudemd.py create mode 100644 tests/test_prompts/test_environment.py create mode 100644 tests/test_prompts/test_system_prompt.py create mode 100644 tests/test_real_large_tasks.py create mode 100644 tests/test_sandbox/test_adapter.py create mode 100644 tests/test_sandbox/test_docker_backend.py create mode 100644 tests/test_sandbox/test_docker_image.py create mode 100644 tests/test_sandbox/test_path_validator.py create mode 100644 tests/test_services/test_autodream.py create mode 100644 tests/test_services/test_autopilot.py create mode 100644 tests/test_services/test_compact.py create mode 100644 tests/test_services/test_cron.py create mode 100644 tests/test_services/test_cron_scheduler.py create mode 100644 tests/test_services/test_session_storage.py create mode 100644 tests/test_skills/test_loader.py create mode 100644 tests/test_swarm/__init__.py create mode 100644 tests/test_swarm/test_imports.py create mode 100644 tests/test_swarm/test_in_process.py create mode 100644 tests/test_swarm/test_lockfile.py create mode 100644 tests/test_swarm/test_mailbox.py create mode 100644 tests/test_swarm/test_permission_sync.py create mode 100644 tests/test_swarm/test_registry.py create mode 100644 tests/test_swarm/test_spawn_utils.py create mode 100644 tests/test_swarm/test_subprocess_backend.py create mode 100644 tests/test_swarm/test_team_lifecycle.py create mode 100644 tests/test_swarm/test_types.py create mode 100644 tests/test_swarm/test_worktree.py create mode 100644 tests/test_tasks/test_manager.py create mode 100644 tests/test_tools/__init__.py create mode 100644 tests/test_tools/test_bash_tool.py create mode 100644 tests/test_tools/test_core_tools.py create mode 100644 tests/test_tools/test_grep_tool.py create mode 100644 tests/test_tools/test_image_generation_tool.py create mode 100644 tests/test_tools/test_image_to_text_tool.py create mode 100644 tests/test_tools/test_integration_flows.py create mode 100644 tests/test_tools/test_mcp_auth_tool.py create mode 100644 tests/test_tools/test_mcp_tool.py create mode 100644 tests/test_tools/test_task_tools.py create mode 100644 tests/test_tools/test_web_fetch_tool.py create mode 100644 tests/test_ui/__init__.py create mode 100644 tests/test_ui/test_coordinator_drain.py create mode 100644 tests/test_ui/test_modes.py create mode 100644 tests/test_ui/test_project_plugin_security.py create mode 100644 tests/test_ui/test_react_backend.py create mode 100644 tests/test_ui/test_react_launcher.py create mode 100644 tests/test_ui/test_runtime_api_key.py create mode 100644 tests/test_ui/test_runtime_close.py create mode 100644 tests/test_ui/test_runtime_plan_mode.py create mode 100644 tests/test_ui/test_runtime_plugin_tools.py create mode 100644 tests/test_ui/test_textual_app.py create mode 100644 tests/test_ui/test_tui_exit_sequence.py create mode 100644 tests/test_untested_features.py create mode 100644 tests/test_utils/test_fs.py create mode 100644 tests/test_utils/test_helpers.py create mode 100644 tests/test_utils/test_network_guard.py create mode 100644 tests/test_utils/test_shell.py diff --git a/.agents/skills b/.agents/skills new file mode 120000 index 0000000..454b842 --- /dev/null +++ b/.agents/skills @@ -0,0 +1 @@ +../.claude/skills \ No newline at end of file diff --git a/.claude/skills/harness-eval/SKILL.md b/.claude/skills/harness-eval/SKILL.md new file mode 100644 index 0000000..671472b --- /dev/null +++ b/.claude/skills/harness-eval/SKILL.md @@ -0,0 +1,193 @@ +--- +name: harness-eval +description: This skill should be used when the user asks to "test the harness", "run integration tests", "validate features with real API", "test with real model calls", "run agent loop tests", "verify end-to-end", or needs to verify OpenHarness features on a real codebase with actual LLM calls. +version: 0.2.0 +--- + +# Harness Eval — End-to-End Feature Validation + +Validate OpenHarness features by running real agent loops against an unfamiliar codebase with actual LLM API calls. Every test exercises the full stack: API client → model → tool calls → execution → result. + +## Core Principles + +1. **Test on an unfamiliar project** — never test on OpenHarness itself (the agent modifies its own code). Clone a real project as the workspace. +2. **Use real API calls** — no mocks. Configure a real LLM endpoint. +3. **Multi-turn conversations** — always test 2+ turns where the model needs prior context. +4. **Combine features** — test hooks+skills+agent loop together, not in isolation. +5. **Verify tool execution** — inspect tool call lists and output files, not just model text. + +## Workflow + +### 1. Prepare Workspace + +Clone an unfamiliar project (do not use OpenHarness): + +```bash +git clone https://github.com/HKUDS/AutoAgent /tmp/eval-workspace +``` + +### 2. Configure Environment + +```bash +export ANTHROPIC_API_KEY=sk-xxx +export ANTHROPIC_BASE_URL=https://api.moonshot.cn/anthropic # or any provider +export ANTHROPIC_MODEL=kimi-k2.5 +``` + +For long-running real evals, do not artificially lower `max_turns`. Use the product default (`200`) unless the user explicitly wants a tighter bound. + +### 3. Prepare Real Sandbox Runtime When Relevant + +If the task is validating sandbox behavior, install and verify the actual runtime before running agent loops: + +```bash +npm install -g @anthropic-ai/sandbox-runtime +sudo apt-get update +sudo apt-get install -y bubblewrap ripgrep +which srt +which bwrap +which rg +srt --version +``` + +Then run a minimal smoke check through OpenHarness, not just raw `srt`, so you verify the real adapter path: + +```python +from pathlib import Path +from openharness.config.settings import Settings, SandboxSettings, save_settings +from openharness.tools.bash_tool import BashTool + +cfg = Path("/tmp/openharness-sandbox-settings.json") +save_settings(Settings(sandbox=SandboxSettings(enabled=True, fail_if_unavailable=True)), cfg) +# Point config loader at this file, then run BashTool on a tiny command such as `pwd`. +``` + +If sandbox dependencies are missing, treat that as an environment/setup failure, not a feature regression. + +### 4. Design Tests + +Each test follows this pattern: + +```python +engine = make_engine(system_prompt="...", cwd=UNFAMILIAR_PROJECT) +evs1 = [ev async for ev in engine.submit_message("Read X, analyze Y")] +r1 = collect(evs1) # text, tools, turns, tokens +evs2 = [ev async for ev in engine.submit_message("Based on what you found...")] +r2 = collect(evs2) +assert "grep" in r1["tools"] # verify tools ran +``` + +For detailed code templates and the `make_engine`/`collect` helpers, consult `references/test-patterns.md`. + +### 5. Prefer Long-Horizon, Real Agent Loops + +For meaningful end-to-end validation, prefer unfamiliar-repo tasks that force multiple turns, context reuse, and mixed tool usage. + +Recommended pattern: + +- Use a real external workspace such as `AutoAgent` +- Use real provider credentials and the actual target model +- Keep `max_turns=200` +- Use per-prompt timeouts large enough for real exploration, such as `240-600s` +- Require at least 2 turns per scenario +- Verify both text quality and tool traces +- Keep polling long-running sessions until they finish; do not abandon a run after the first long pause + +Recommended long-horizon scenarios: + +- `architecture_multiturn` + - Turn 1: map architecture, shell/subprocess surfaces, and test entrypoints + - Turn 2: identify top risks and propose refactors + - Turn 3: condense into onboarding or remediation actions + - Success: `bash`, `glob`, `grep`, `read_file` all appear; no timeout; no `MaxTurnsExceeded` + +- `hook_block_and_recover` + - Force the model to try `bash` + - Block it with a real pre-tool hook + - Verify the model adapts with `glob`/`grep`/`read_file` + +- `sandbox_multiturn` + - Enable real sandbox settings with `fail_if_unavailable=true` + - First prompt must start with exactly one shell command such as `pwd && ls -la` + - Second prompt must explicitly reuse the prior shell findings + - Success: `bash` executes via sandbox, non-shell tools continue the task, and the agent recovers from incidental repo errors + +When a scenario fails, classify it before changing code: + +- `MaxTurnsExceeded`: likely eval harness misconfiguration if `max_turns` was manually lowered +- `timeout`: task is too broad or per-prompt timeout is too small +- sandbox unavailable: environment missing `srt`, `bwrap`, or `rg` +- tool error with task still completed: feature may still be healthy; inspect recovery behavior + +### 6. Run Tests + +```bash +python tests/test_merged_prs_on_autoagent.py # PR feature tests +python tests/test_real_large_tasks.py # large multi-step tasks +python tests/test_hooks_skills_plugins_real.py # hooks/skills/plugins +python -m pytest tests/ -q -k "not autoagent" # unit tests (no API) +``` + +For ad hoc long-horizon validation, it is acceptable to run a temporary Python driver script as long as it: + +- uses real OpenHarness engine/tool objects +- targets an unfamiliar repository +- prints per-scenario JSON summaries +- records tools, errors, turns, and token usage +- stays attached until completion + +### 7. Interpret Results + +| Result | Meaning | Action | +|--------|---------|--------| +| PASS with tool calls | Feature works end-to-end | Done | +| PASS without tool calls | Model answered from knowledge | Rewrite prompt to force tool use | +| FAIL with exception | Code bug | Read traceback | +| FAIL with wrong output | Model behavior issue | Check system prompt and tool schemas | +| Timeout | Task too complex | Increase `max_turns` or simplify prompt | + +For long-running real evals, refine the timeout guidance: + +- First check whether `max_turns` was manually set too low +- If `max_turns=200` and the run still fails, the next suspect is wall-clock timeout, not turn count +- Distinguish environment failures from product failures + - Example: missing dependency in the unfamiliar target repo is not automatically an OpenHarness regression + - Example: missing `srt`/`bwrap`/`rg` is an eval environment issue + +## Feature Coverage Checklist + +- [ ] Engine: multi-turn memory, tool chaining, parallel tools, error recovery, auto-compaction +- [ ] Swarm: InProcessBackend lifecycle, concurrent teammates, coordinator+notifications +- [ ] Hooks: pre_tool_use blocking → model adapts, post_tool_use firing +- [ ] Skills: skill tool invocation → model follows instructions +- [ ] Plugins: plugin-provided skill loaded and used in agent loop +- [ ] Memory: YAML frontmatter parsing, body content search, context injection +- [ ] Session: save → load → resume with context preserved +- [ ] Providers: Anthropic client, OpenAI client (with reasoning_content), multi-turn +- [ ] Cost: token accumulation across turns + +## Common Pitfalls + +- Testing on OpenHarness itself — agent modifies its own running code +- Using mocks — misses serialization and API compatibility bugs +- Single-turn only — misses context accumulation and compaction bugs +- Artificially lowering `max_turns` during real evals — can create false failures that do not reflect product defaults +- Not checking tool call list — model may claim tool use without calling it +- Hardcoding paths — use `WORKSPACE` variable, skip in CI with `pytest.mark.skipif` +- Declaring sandbox “tested” after only checking raw `srt` — verify the OpenHarness adapter path too +- Abandoning long tasks too early — some real tasks pause for minutes before the next event arrives + +## Additional Resources + +### Reference Files + +- **`references/test-patterns.md`** — Complete code templates for `make_engine`, `collect`, and each feature category +- **`references/feature-matrix.md`** — Detailed test cases for every OpenHarness module + +### Existing Test Files + +Working test suites in the repo: +- `tests/test_merged_prs_on_autoagent.py` — PR feature validation +- `tests/test_real_large_tasks.py` — Large multi-step tasks +- `tests/test_hooks_skills_plugins_real.py` — Hooks/skills/plugins in agent loops +- `tests/test_untested_features.py` — Module-level integration tests diff --git a/.claude/skills/harness-eval/references/feature-matrix.md b/.claude/skills/harness-eval/references/feature-matrix.md new file mode 100644 index 0000000..175d4f1 --- /dev/null +++ b/.claude/skills/harness-eval/references/feature-matrix.md @@ -0,0 +1,47 @@ +# Feature Test Matrix — Detailed Test Cases + +## Engine & Tools + +| Test | What to Verify | Key Assertion | +|------|---------------|---------------| +| Multi-turn memory | Set fact turn 1, recall turn 3 | Fact appears in turn 3 response | +| Tool chaining | glob → grep → read in one task | All 3 tools in tool list | +| Write→Edit→Read | Create, modify, verify file | File content matches expected | +| Parallel tools | 3+ tool calls in one response | 3+ tools in single turn | +| Error recovery | Tool fails, model adapts | Alternative tool used after error | +| Auto-compaction | 5+ tasks on shared engine | No context overflow crash | + +## Swarm & Coordinator + +| Test | What to Verify | Key Assertion | +|------|---------------|---------------| +| InProcessBackend | spawn → active → status → shutdown | All states transition correctly | +| Concurrent teammates | 2+ agents running simultaneously | Both complete, total time < 2x single | +| Coordinator + notifications | Multi-turn delegation with XML | Coordinator synthesizes worker results | +| Permission sync | request → pending → resolve | Pending count goes to 0 after resolve | + +## Hooks, Skills, Plugins + +| Test | What to Verify | Key Assertion | +|------|---------------|---------------| +| Hook blocks → adapt | pre_tool_use blocks bash | "bash" in errors, "glob" in tools | +| Skill invocation | Model calls skill tool | "skill" in tools, content drives next action | +| Plugin skill | Plugin provides skill | Loaded via skill tool, model follows it | +| Hook + skill combined | Hook gates writes, skill guides | Protected file untouched, new file created | + +## Memory, Session, Config + +| Test | What to Verify | Key Assertion | +|------|---------------|---------------| +| Memory frontmatter | YAML parsed, not "---" | description != "---", body searchable | +| Session resume | Save → load → continue | Model remembers prior context | +| Cost tracking | Tokens accumulate | in_tokens strictly increasing | +| Cron CRUD | Create, toggle, mark_run, delete | Job count correct at each step | + +## Provider Compatibility + +| Test | What to Verify | Key Assertion | +|------|---------------|---------------| +| Anthropic client | Standard tool calling | Tools execute, response coherent | +| OpenAI client | Tool calling + reasoning_content | No 400 error on tool call round-trip | +| OpenAI multi-turn | reasoning_content persists | 3+ turns without API error | diff --git a/.claude/skills/harness-eval/references/test-patterns.md b/.claude/skills/harness-eval/references/test-patterns.md new file mode 100644 index 0000000..2661c17 --- /dev/null +++ b/.claude/skills/harness-eval/references/test-patterns.md @@ -0,0 +1,150 @@ +# Test Patterns — Code Templates for Harness Eval + +## Engine Setup Helpers + +```python +import asyncio, sys, os +from pathlib import Path +sys.path.insert(0, "src") + +API_KEY = os.environ.get("ANTHROPIC_API_KEY", "your-key") +BASE_URL = os.environ.get("ANTHROPIC_BASE_URL", "https://api.moonshot.cn/anthropic") +OPENAI_BASE = os.environ.get("OPENAI_BASE_URL", "https://api.moonshot.cn/v1") +MODEL = os.environ.get("ANTHROPIC_MODEL", "kimi-k2.5") +WORKSPACE = Path("/tmp/eval-workspace") # unfamiliar project + + +def make_anthropic_engine(system_prompt, cwd=None, extra_tools=None): + from openharness.api.client import AnthropicApiClient + from openharness.config.settings import PermissionSettings + from openharness.engine.query_engine import QueryEngine + from openharness.permissions.checker import PermissionChecker + from openharness.permissions.modes import PermissionMode + from openharness.tools.base import ToolRegistry + from openharness.tools.bash_tool import BashTool + from openharness.tools.file_read_tool import FileReadTool + from openharness.tools.file_write_tool import FileWriteTool + from openharness.tools.file_edit_tool import FileEditTool + from openharness.tools.glob_tool import GlobTool + from openharness.tools.grep_tool import GrepTool + + api = AnthropicApiClient(api_key=API_KEY, base_url=BASE_URL) + reg = ToolRegistry() + for t in [BashTool(), FileReadTool(), FileWriteTool(), FileEditTool(), GlobTool(), GrepTool()]: + reg.register(t) + for t in (extra_tools or []): + reg.register(t) + checker = PermissionChecker(PermissionSettings(mode=PermissionMode.FULL_AUTO)) + return QueryEngine( + api_client=api, tool_registry=reg, permission_checker=checker, + cwd=Path(cwd or WORKSPACE), model=MODEL, system_prompt=system_prompt, max_tokens=4096, + ) + + +def make_openai_engine(system_prompt, cwd=None, extra_tools=None): + from openharness.api.openai_client import OpenAICompatibleClient + # Same structure as above, but with: + api = OpenAICompatibleClient(api_key=API_KEY, base_url=OPENAI_BASE) + # ... rest identical + + +def collect(events): + from openharness.engine.stream_events import ( + AssistantTextDelta, AssistantTurnComplete, + ToolExecutionStarted, ToolExecutionCompleted, + ) + r = {"text": "", "tools": [], "turns": 0, "in_tok": 0, "out_tok": 0} + for ev in events: + if isinstance(ev, AssistantTextDelta): + r["text"] += ev.text + elif isinstance(ev, ToolExecutionStarted): + r["tools"].append(ev.tool_name) + elif isinstance(ev, AssistantTurnComplete): + r["turns"] += 1 + r["in_tok"] += ev.usage.input_tokens + r["out_tok"] += ev.usage.output_tokens + return r +``` + +## Multi-Turn Memory Test + +```python +async def test_multi_turn_memory(): + engine = make_anthropic_engine("Remember what the user tells you.") + [ev async for ev in engine.submit_message("My favorite number is 42.")] + [ev async for ev in engine.submit_message("What is 2+2?")] + evs = [ev async for ev in engine.submit_message("What is my favorite number?")] + r = collect(evs) + assert "42" in r["text"] +``` + +## Hook Blocks Tool → Model Adapts + +```python +async def test_hook_blocks(): + from openharness.hooks.events import HookEvent + from openharness.hooks.loader import HookRegistry + from openharness.hooks.schemas import CommandHookDefinition + from openharness.hooks.executor import HookExecutor, HookExecutionContext + + hook_reg = HookRegistry() + hook_reg.register(HookEvent.PRE_TOOL_USE, CommandHookDefinition( + type="command", command="exit 1", + matcher="bash", block_on_failure=True, timeout_seconds=5, + )) + # ... create engine with hook_executor, model tries bash, gets blocked, adapts to glob +``` + +## Skill Tool Invocation + +```python +async def test_skill_invocation(): + from openharness.tools.skill_tool import SkillTool + engine = make_anthropic_engine( + "Use the 'skill' tool to load instructions before working.", + extra_tools=[SkillTool()], + ) + evs = [ev async for ev in engine.submit_message( + "Load the 'diagnose' skill, then investigate the codebase." + )] + r = collect(evs) + assert "skill" in r["tools"] +``` + +## InProcess Concurrent Teammates + +```python +async def test_concurrent_teammates(): + from openharness.swarm.in_process import start_in_process_teammate, TeammateAbortController + from openharness.swarm.types import TeammateSpawnConfig + from openharness.engine.query import QueryContext + + async def run_one(name, prompt): + ctx = QueryContext(api_client=api, tool_registry=reg, ...) + config = TeammateSpawnConfig(name=name, team="test", prompt=prompt, ...) + abort = TeammateAbortController() + await start_in_process_teammate(config=config, agent_id=f"{name}@test", abort_controller=abort, query_context=ctx) + + await asyncio.gather( + asyncio.wait_for(run_one("worker-a", "Count .py files"), timeout=30), + asyncio.wait_for(run_one("worker-b", "Find main class"), timeout=30), + ) +``` + +## Session Save → Resume + +```python +async def test_session_resume(): + from openharness.services.session_storage import save_session_snapshot, load_session_snapshot + from openharness.engine.messages import ConversationMessage + + engine1 = make_anthropic_engine("Remember context.") + [ev async for ev in engine1.submit_message("Project uses FastAPI + React.")] + save_session_snapshot(cwd=tmpdir, model=MODEL, system_prompt="...", messages=engine1.messages, usage=engine1.total_usage) + + loaded = load_session_snapshot(tmpdir) + engine2 = make_anthropic_engine("Continue analysis.") + engine2.load_messages([ConversationMessage.model_validate(m) for m in loaded["messages"]]) + evs = [ev async for ev in engine2.submit_message("What tech stack did I mention?")] + assert "fastapi" in collect(evs)["text"].lower() +``` diff --git a/.claude/skills/pr-merge/SKILL.md b/.claude/skills/pr-merge/SKILL.md new file mode 100644 index 0000000..fd0908a --- /dev/null +++ b/.claude/skills/pr-merge/SKILL.md @@ -0,0 +1,99 @@ +--- +name: pr-merge +description: This skill should be used when the user asks to "merge a PR", "review and merge pull requests", "integrate external contributions", "handle PR conflicts", "cherry-pick from a PR", or needs to merge GitHub PRs while maximizing contributor attribution. +version: 0.1.0 +--- + +# PR Merge — Contributor-First Pull Request Integration + +Merge external pull requests while maximizing original author attribution. Core principle: **merge first, resolve conflicts after** — never rewrite a contributor's work from scratch. + +## Core Principles + +1. **Preserve authorship** — use `gh pr merge --squash` for clean PRs. For manual merges, use `--author="Name "`. +2. **Merge first, fix after** — accept the PR's approach even if it differs from local style. Fix conflicts in a separate commit. +3. **Selective merge is OK** — exclude files with `--exclude` when a PR contains features already implemented locally. Document what was excluded. +4. **Never `git apply` + self-commit** — this loses author attribution entirely. + +## Workflow + +### 1. Triage Open PRs + +```bash +gh pr list --repo OWNER/REPO --state open \ + --json number,title,author,additions,deletions,mergeable +``` + +Classify each PR: merge directly, merge with conflict resolution, selective merge, or close. + +### 2. Merge Clean PRs via GitHub + +Prefer `gh pr merge` — preserves author automatically: + +```bash +gh pr merge NUMBER --repo OWNER/REPO --squash \ + --subject "feat: description (#NUMBER)" +``` + +### 3. Handle Conflicting PRs Locally + +```bash +git fetch origin pull/NUMBER/head:pr-NUMBER +git merge pr-NUMBER --no-edit +# Resolve conflicts keeping both sides where possible +git add -A && git commit --no-edit +``` + +### 4. Selective Merge (Skip Some Files) + +When a PR contains features already implemented locally: + +```bash +gh pr diff NUMBER --repo OWNER/REPO | \ + git apply --exclude='path/to/skip.py' --exclude='CHANGELOG.md' +git commit --author="Author Name " \ + -m "feat: description (#NUMBER) + +Cherry-picked from PR #NUMBER. Excluded: file.py (already implemented)." +``` + +### 5. Close Duplicate/Superseded PRs + +```bash +gh pr close NUMBER --repo OWNER/REPO \ + --comment "Fixed via PR #OTHER. Thank you for the contribution!" +``` + +### 6. Post-Merge Verification + +```bash +python -m ruff check src/ tests/ # lint +python -m pytest tests/ -q # unit tests +git push origin main # push +``` + +Then run `harness-eval` for end-to-end verification on an unfamiliar codebase. + +## Attribution Checklist + +Before pushing a merged PR: + +- [ ] Original author appears in `git log` (via `--author` or GitHub squash merge) +- [ ] Commit message references PR number (`#NUMBER`) +- [ ] If selectively merged, commit body explains exclusions +- [ ] Closed PRs have a comment thanking the contributor +- [ ] Duplicate PRs acknowledge the contributor's investigation + +## Common Pitfalls + +- `git apply` + self-commit loses author +- Rewriting from scratch instead of merging — merge their code, fix style after +- Force-pushing main after merge — may remove contributor commits +- Forgetting CHANGELOG conflicts — always exclude and handle manually +- Not testing after merge — clean merge doesn't mean working code + +## Additional Resources + +### Reference Files + +- **`references/merge-scenarios.md`** — Detailed examples for each merge scenario (clean, conflicting, selective, duplicate) diff --git a/.claude/skills/pr-merge/references/merge-scenarios.md b/.claude/skills/pr-merge/references/merge-scenarios.md new file mode 100644 index 0000000..196fe24 --- /dev/null +++ b/.claude/skills/pr-merge/references/merge-scenarios.md @@ -0,0 +1,131 @@ +# Merge Scenarios — Detailed Examples + +## Scenario 1: Clean Merge (No Conflicts) + +PR adds a new feature, no files overlap with local changes. + +```bash +# Preferred: squash merge via GitHub (preserves author) +gh pr merge 17 --repo HKUDS/OpenHarness --squash \ + --subject "feat(skills): add diagnose skill (#17)" + +# Result: author "Qu Zhi" appears in git log +``` + +## Scenario 2: CHANGELOG Conflict Only + +Almost every PR touches CHANGELOG.md. Handle by excluding it: + +```bash +gh pr diff 14 --repo OWNER/REPO | \ + git apply --exclude='CHANGELOG.md' +git add -A + +# Manually merge CHANGELOG sections (keep both) +# Then commit with original author +git commit --author="washi4 " \ + -m "feat: add OpenAI-compatible client (#14)" +``` + +## Scenario 3: Conflicting UI Files + +PR #14 modified ui/app.py and ui/backend_host.py which were also changed by PR #13. + +```bash +# Apply excluding conflicting files +gh pr diff 14 --repo OWNER/REPO | \ + git apply --exclude='src/ui/app.py' --exclude='src/ui/backend_host.py' --exclude='CHANGELOG.md' + +# Manually add the PR's changes to conflicting files +# Read the PR diff to understand what they added, apply by hand +# Commit with author +git commit --author="Author " -m "feat: description (#14)" +``` + +## Scenario 4: Selective Merge (Skip Features) + +PR #16 has auto-compact (already implemented locally) + --resume (wanted) + cron (wanted). + +```bash +# Exclude the file containing the unwanted feature +gh pr diff 16 --repo OWNER/REPO | git apply \ + --exclude='src/engine/query_engine.py' \ # skip auto-compact + --exclude='CHANGELOG.md' \ + --exclude='README.md' + +# Commit with explanation +git commit --author="Chao Qin " \ + -m "feat: wire --resume/--continue and cron scheduler (#16) + +Cherry-picked from PR #16. Excluded auto-compact (already implemented +with LLM-based approach from reference source)." +``` + +## Scenario 5: Duplicate PRs (Same Bug, Different Fix) + +PR #11 and #13 both fix the double-Enter bug. #13 is smaller and cleaner. + +```bash +# Merge #13 (the better fix) +gh pr merge 13 --repo OWNER/REPO --squash + +# Close #11 with acknowledgment +gh pr close 11 --repo OWNER/REPO --comment \ + "Fixed via PR #13 (smaller patch for the same bug). \ +Thank you for the detailed investigation!" +``` + +## Scenario 6: PR From a Fork + +The contributor pushed to their fork, not a branch on the repo. + +```bash +# Fetch via PR ref (works for any PR regardless of source) +git fetch origin pull/14/head:pr-14 +git merge pr-14 --no-edit + +# If merge conflict: +git mergetool # or manually resolve +git add -A && git commit --no-edit + +git push origin main +``` + +## Scenario 7: Batch Merge (Multiple PRs) + +Merge in dependency order, test after each: + +```bash +# 1. Small fixes first (least risk) +gh pr merge 17 --squash # skill file +gh pr merge 13 --squash # 1-file bug fix + +# 2. Medium changes +gh pr merge 12 --squash # memory improvement + +# 3. Large features last (most conflict risk) +# PR #14 needs manual conflict resolution +git fetch origin pull/14/head:pr-14 +git merge pr-14 +# resolve CHANGELOG conflict +git push origin main + +# 4. Test after all merges +python -m ruff check src/ tests/ +python -m pytest tests/ -q +``` + +## Post-Merge Fix Commit Pattern + +When the merged PR introduces a compatibility issue (e.g., API mismatch with a provider): + +```bash +# Fix in a SEPARATE commit (don't amend the author's commit) +git commit -m "fix(api): handle Kimi reasoning_content in OpenAI client + +Kimi k2.5 requires reasoning_content on assistant tool_call messages. +Fix: capture during streaming, replay when converting back. +Found during post-merge testing with harness-eval." +``` + +This preserves the original author's commit intact while documenting the fix separately. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..7020d3a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,41 @@ +name: Bug report +description: Report a reproducible problem in OpenHarness +title: "[Bug]: " +labels: + - bug +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug. Please include concrete steps, environment details, and error output. + - type: textarea + id: summary + attributes: + label: What happened? + description: Describe the bug and the behavior you expected. + validations: + required: true + - type: textarea + id: repro + attributes: + label: Steps to reproduce + description: Include commands, prompts, or config that reliably reproduces the issue. + placeholder: | + 1. Run `uv run oh ...` + 2. Trigger ... + 3. Observe ... + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + description: OS, Python version, Node version if relevant, provider/base URL, and install method. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant logs or screenshots + description: Paste error output, stack traces, or terminal screenshots. + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..566aa7a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Contribution guide + url: https://github.com/HKUDS/OpenHarness/blob/main/CONTRIBUTING.md + about: Read local setup, validation, and PR expectations before contributing. + - name: Showcase ideas + url: https://github.com/HKUDS/OpenHarness/blob/main/docs/SHOWCASE.md + about: See example workflows and suggest new real-world use cases. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..38c1d0a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,33 @@ +name: Feature request +description: Propose a focused improvement or missing workflow +title: "[Feature]: " +labels: + - enhancement +body: + - type: markdown + attributes: + value: | + Please describe the workflow gap as concretely as possible. Small, scoped requests are easier to prioritize. + - type: textarea + id: problem + attributes: + label: What workflow is missing or painful today? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed change + description: Explain the behavior you want and any relevant CLI, UI, or provider details. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: If you have a workaround or competing approach, include it here. + - type: textarea + id: extra + attributes: + label: Additional context + description: Links to related issues, code references, or benchmarks. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..69c7993 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## Summary + +- What problem does this PR solve? +- What changed? + +## Validation + +- [ ] `uv run ruff check src tests scripts` +- [ ] `uv run pytest -q` +- [ ] `cd frontend/terminal && npx tsc --noEmit` (if frontend touched) + +## Notes + +- Related issue: +- Follow-up work: diff --git a/.github/workflows/autopilot-pages.yml b/.github/workflows/autopilot-pages.yml new file mode 100644 index 0000000..4dfcd20 --- /dev/null +++ b/.github/workflows/autopilot-pages.yml @@ -0,0 +1,59 @@ +name: Autopilot Pages + +on: + push: + branches: + - main + paths: + - "docs/autopilot/**" + - "autopilot-dashboard/**" + - ".github/workflows/autopilot-pages.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: autopilot-pages + cancel-in-progress: true + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: autopilot-dashboard/package-lock.json + + - name: Install dependencies + working-directory: autopilot-dashboard + run: npm ci + + - name: Build dashboard + working-directory: autopilot-dashboard + run: npm run build + + - name: Configure Pages + uses: actions/configure-pages@v5 + with: + enablement: true + + - name: Upload autopilot dashboard artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/autopilot + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/autopilot-run-next.yml b/.github/workflows/autopilot-run-next.yml new file mode 100644 index 0000000..30f8808 --- /dev/null +++ b/.github/workflows/autopilot-run-next.yml @@ -0,0 +1,53 @@ +name: Autopilot Run Next + +on: + schedule: + - cron: "0 */2 * * *" + workflow_dispatch: + +permissions: + contents: write + issues: write + pull-requests: write + +concurrency: + group: autopilot-run-next + cancel-in-progress: true + +jobs: + run-next: + runs-on: self-hosted + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + clean: false + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --extra dev + + - name: Execute autopilot tick + run: uv run python -m openharness autopilot tick --cwd "$GITHUB_WORKSPACE" + + - name: Export dashboard + run: uv run python -m openharness autopilot export-dashboard --cwd "$GITHUB_WORKSPACE" + + - name: Commit dashboard updates + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add docs/autopilot + git diff --cached --quiet && exit 0 + git commit -m "autopilot: refresh dashboard after run" + git push diff --git a/.github/workflows/autopilot-scan.yml b/.github/workflows/autopilot-scan.yml new file mode 100644 index 0000000..760812b --- /dev/null +++ b/.github/workflows/autopilot-scan.yml @@ -0,0 +1,53 @@ +name: Autopilot Scan + +on: + schedule: + - cron: "*/30 * * * *" + workflow_dispatch: + +permissions: + contents: write + issues: write + pull-requests: write + +concurrency: + group: autopilot-scan + cancel-in-progress: true + +jobs: + scan: + runs-on: self-hosted + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + clean: false + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --extra dev + + - name: Scan autopilot intake + run: uv run python -m openharness autopilot scan all --cwd "$GITHUB_WORKSPACE" + + - name: Export dashboard + run: uv run python -m openharness autopilot export-dashboard --cwd "$GITHUB_WORKSPACE" + + - name: Commit dashboard updates + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add docs/autopilot + git diff --cached --quiet && exit 0 + git commit -m "autopilot: refresh dashboard after scan" + git push diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8674a29 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,83 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +jobs: + python-tests: + name: Python tests (${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - "3.10" + - "3.11" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --extra dev + + - name: Run test suite + run: uv run pytest -q + + python-quality: + name: Python quality + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Set up uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --extra dev + + - name: Ruff + run: uv run ruff check src tests scripts + + frontend-typecheck: + name: Frontend typecheck + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend/terminal + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: frontend/terminal/package-lock.json + + - name: Install frontend dependencies + run: npm ci + + - name: TypeScript check + run: npx tsc --noEmit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d7d05be --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +__pycache__/ +*.py[cod] +*$py.class +*.egg-info/ +dist/ +build/ +.eggs/ +*.egg +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.venv/ +.openharness-venv/ +venv/ +env/ +.env +*.so +.coverage +htmlcov/ +.idea/ +.vscode/ +*.swp +*.swo +*~ +.python-version + +# React TUI frontend +frontend/terminal/node_modules/ + +# User data (never commit API keys or session data) +.openharness/ +react_tui_smoke.txt + +# OS +.DS_Store +Thumbs.db +uv.lock diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..351dec9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,105 @@ +# Changelog + +All notable changes to OpenHarness should be recorded in this file. + +The format is based on Keep a Changelog, and this project currently tracks changes in a lightweight, repository-oriented way. + +## [Unreleased] + +### Added + +- Hooks now support a `priority` field (default `0`). Within an event, hooks run highest-priority first, and hooks sharing a priority keep their registration order. This lets users order, for example, a security-check hook ahead of a logging hook regardless of where each is declared in settings or contributed by plugins. +- `edit_file` and `write_file` in the React TUI now preview a unified diff before applying file changes, let users approve once or for the rest of the session, and skip the extra prompt automatically in `full_auto` mode. + +### Fixed + +- Codex subscription requests now pass reasoning effort separately, enabling `gpt-5.5` with `xhigh` effort instead of treating `gpt-5.5 xhigh` as an unsupported model name. +- Telegram channel now delivers replies again under `ohmo init --no-interactive` and other configs that do not write a `reply_to_message` field. `TelegramConfig` declares `reply_to_message: bool = True` so the attribute access in `TelegramChannel.send` no longer raises `AttributeError` and outbound progress/tool-hint/final messages are sent as expected. See issue #243. + +## [0.1.9] - 2026-05-07 + +### Added + +- Added a bundled `skill-creator` skill for creating, improving, and verifying OpenHarness/ohmo skills. +- User-invocable skills can now be triggered directly as slash commands, with support for skill-specific arguments and model override metadata. + +### Fixed + +- `oh setup` can now update the API key for an already-configured API-key provider profile instead of only changing the model. +- `oh provider edit --api-key ` can now replace a saved profile API key, and `oh provider add ... --api-key ` can store one during profile creation. + +## [0.1.8] - 2026-05-06 + +### Added + +- Built-in `nvidia` provider profile so `oh setup` offers NVIDIA NIM as a first-class OpenAI-compatible provider choice, with `NVIDIA_API_KEY` auth source, `openai/gpt-oss-120b` as the default model, and the NVIDIA NIM endpoint. +- Built-in `qwen` provider profile so `oh setup` offers Qwen (DashScope) as a first-class provider choice, with `dashscope_api_key` auth source, `qwen-plus` as the default model, and the DashScope OpenAI-compatible endpoint. +- Plugin tool discovery: plugins can now provide `BaseTool` subclasses in a `/tools/` directory and they are auto-discovered, instantiated, and registered in the tool registry at runtime. Add `tools_dir` to `plugin.json` (defaults to `"tools"`). +- `oh --dry-run` safe preview mode for inspecting resolved runtime settings, auth state, prompt assembly, commands, skills, tools, and configured MCP servers without executing the model or tools. +- Built-in `minimax` provider profile so `oh setup` offers MiniMax as a first-class provider choice, with `MINIMAX_API_KEY` auth source, `MiniMax-M2.7` as the default model, and `MiniMax-M2.7-highspeed` in the model picker. +- Docker as an alternative sandbox backend (`sandbox.backend = "docker"`) for stronger execution isolation with configurable resource limits, network isolation, and automatic image management. +- Built-in `gemini` provider profile so `oh setup` offers Google Gemini as a first-class provider choice, with `gemini_api_key` auth source and `gemini-2.5-flash` as the default model. +- `diagnose` skill: trace agent run failures and regressions using structured evidence from run artifacts. +- OpenAI-compatible API client (`--api-format openai`) supporting any provider that implements the OpenAI `/v1/chat/completions` format, including Alibaba DashScope, DeepSeek, GitHub Models, Groq, Together AI, Ollama, and more. +- `OPENHARNESS_API_FORMAT` environment variable for selecting the API format. +- `OPENAI_API_KEY` fallback when using OpenAI-format providers. +- GitHub Actions CI workflow for Python linting, tests, and frontend TypeScript checks. +- `CONTRIBUTING.md` with local setup, validation commands, and PR expectations. +- `docs/SHOWCASE.md` with concrete OpenHarness usage patterns and demo commands. +- GitHub issue templates and a pull request template. +- React TUI assistant messages now render structured Markdown blocks, including headings, lists, code fences, blockquotes, links, and tables. +- Built-in `codex` output style for compact, low-noise transcript rendering in React TUI. + +### Fixed + +- Subprocess teammate spawn (`agent` tool, `task_create`) now works on Windows under Git Bash. `subprocess_backend.spawn` builds a direct-exec `argv` list and passes it through new `argv=` and `env=` kwargs on `BackgroundTaskManager.create_agent_task` / `create_shell_task`; `_start_process` then runs the executable via `asyncio.create_subprocess_exec(*argv)` with no shell in between. Previously the spawn command was a single string interpreted by `bash -lc`, which on Windows could not reliably exec a Windows-pathed Python interpreter (e.g. `C:\Users\...\python.exe`) — Git Bash's escape parser consumed the backslashes from the embedded env-prefix and, even with proper quoting, bash launched via `asyncio.create_subprocess_exec` returned `command not found` for Windows-pathed binaries that worked perfectly when invoked interactively. Bypassing the shell sidesteps the entire class of cross-platform quoting and path-translation hazard. The legacy shell-evaluated `command=` path is preserved for callers (e.g. `BashTool`) that legitimately want shell semantics. See issue #230. +- Bundled skill loader now uses `yaml.safe_load` for SKILL.md frontmatter, matching the user-skill loader. The shared parser is extracted to `openharness.skills._frontmatter` so bundled and user skills handle YAML block scalars (`>`, `|`), quoted values, and other standard YAML constructs the same way. +- Compaction now detects llama.cpp/OpenAI-compatible context overflow errors, accounts for image blocks in auto-compact token estimates, and strips image payloads from summarizer-only compaction requests. +- Large tool results are now bounded in conversation history: oversized outputs are saved under `tool_artifacts`, old MCP results become microcompactable, and context collapse trims stale tool-result payloads. +- ohmo now keeps personal memory isolated from OpenHarness project memory: `/memory` in ohmo sessions targets the ohmo workspace memory store, and ohmo runtime prompt refreshes no longer inject project memory unless explicitly requested. +- Fixed `glob` and `grep` tools hanging indefinitely when the `rg` subprocess produced enough stderr output to fill the OS pipe buffer. `stderr` is now redirected to `DEVNULL` so it is discarded rather than blocking the child process. +- Fixed `bash_tool` hanging after a timed-out command when the subprocess stdout stream stayed open. `_read_remaining_output` now applies a 2-second `asyncio.wait_for` timeout so the tool always returns promptly. +- Fixed `session_runner` background task deadlock caused by an unread `stderr=PIPE` stream. The subprocess now uses `stderr=STDOUT` so all output merges into the single readable stdout pipe. +- React TUI prompt input now treats the raw DEL byte (`0x7f`) as backward delete while preserving true forward-delete escape sequences, fixing backspace failures seen in some macOS terminal environments. +- `todo_write` tool now updates an existing unchecked item in-place when `checked=True` instead of appending a duplicate `[x]` line. + +- Built-in `Explore` and `claude-code-guide` agents no longer hard-code `model="haiku"`, which caused them to fail for users on non-Anthropic providers (OpenAI, Bedrock, custom base URLs, etc.). Both agents now use `model="inherit"` so they run with whatever model the parent session is using. `build_inherited_cli_flags` is also fixed to skip the `--model` flag entirely when the value is `"inherit"`, letting the subprocess correctly inherit the parent model via the `OPENHARNESS_MODEL` environment variable instead of receiving the literal string `"inherit"` as a model name. + +- React TUI spinner now stays visible throughout the entire agent turn: `assistant_complete` no longer resets `busy` state prematurely, and `tool_started` explicitly sets `busy=true` so the status bar remains active even when tool calls follow an assistant message. `line_complete` is the sole signal that ends the turn and clears the spinner. +- Skill loader now uses `yaml.safe_load` to parse SKILL.md frontmatter, correctly handling YAML block scalars (`>`, `|`), quoted values, and other standard YAML constructs instead of naive line-by-line splitting. +- `BackendHostConfig` was missing the `cwd` field, causing `AttributeError: 'BackendHostConfig' object has no attribute 'cwd'` on startup when `oh` was run after the runtime refactor that added `cwd` support to `build_runtime`. +- Shell-escape `$ARGUMENTS` substitution in command hooks to prevent shell injection from payload values containing metacharacters like `$(...)` or backticks. +- Swarm `_READ_ONLY_TOOLS` now uses actual registered tool names (snake_case) instead of PascalCase, fixing read-only auto-approval in `handle_permission_request`. +- Memory scanner now parses YAML frontmatter (`name`, `description`, `type`) instead of returning raw `---` as description. +- Memory search matches against body content in addition to metadata, with metadata weighted higher for relevance. +- Memory search tokenizer handles Han characters for multilingual queries. +- Fixed duplicate response in React TUI caused by double Enter key submission in the input handler. +- Fixed concurrent permission modals overwriting each other in TUI default mode when the LLM returns multiple tool calls in one response; `_ask_permission` now serialises callers via an `asyncio.Lock` so each modal is shown and resolved before the next one is emitted. +- Fixed React TUI Markdown tables to size columns from rendered cell text so inline formatting like code spans and bold text no longer breaks alignment. +- Fixed grep tool crashing with `ValueError` / `LimitOverrunError` when ripgrep outputs a line longer than 64 KB (e.g. minified assets or lock files). The asyncio subprocess stream limit is now 8 MB and oversized lines are skipped rather than terminating the session. +- Fixed React TUI exit leaving the shell prompt concatenated with the last TUI line. The terminal cleanup handler now writes a trailing newline (`\n`) alongside the cursor-show escape sequence so the shell prompt always starts on a fresh line. +- Reduced React TUI redraw pressure when `output_style=codex` by avoiding token-level assistant buffer flushes during streaming. + +### Changed + +- ohmo Feishu group routing now supports managed group creation, gateway-scoped provider/model commands, and stricter group mention handling so group conversations only wake ohmo when explicitly addressed. +- Dry-run output now reports a `ready` / `warning` / `blocked` readiness verdict, concrete `next_actions`, likely matching skills/tools for normal prompts, and richer slash-command previews for read-only vs stateful command paths. +- React TUI now groups consecutive `tool` + `tool_result` transcript rows into a single compound row: success shows the result line count inline (e.g. `→ 24L`), errors show a red icon and up to 5 lines of error detail beneath the tool row. Standalone successful tool results are suppressed to reduce transcript noise; standalone errors are still surfaced. +- README now links to contribution docs, changelog, showcase material, and provider compatibility guidance. +- README quick start now includes a one-command demo and clearer provider compatibility notes. +- README provider compatibility section updated to include OpenAI-format providers. + +## [0.1.7] - 2026-04-18 + +### Fixed + +- Install script now links `oh`, `ohmo`, and `openharness` into `~/.local/bin` instead of prepending the virtualenv `bin` directory to `PATH`, which avoids overriding Conda-managed shells while preserving global command discovery. +- React TUI prompt now supports `Shift+Enter` for inserting a newline without submitting the current prompt. +- React TUI busy-state animation is less error-prone on Windows terminals: the extra pseudo-animation line was removed, Windows now uses conservative ASCII spinner frames, and the spinner interval was slightly slowed to reduce flashing. + +## [0.1.0] - 2026-04-01 + +### Added + +- Initial public release of OpenHarness. +- Core agent loop, tool registry, permission system, hooks, skills, plugins, MCP support, and terminal UI. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2fd2154 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,68 @@ +# Contributing to OpenHarness + +OpenHarness is an open-source agent harness focused on clarity, hackability, and compatibility with Claude-style workflows. + +## Ways to contribute + +- Fix bugs or tighten edge-case handling in the harness runtime. +- Improve docs, onboarding, examples, and architecture notes. +- Add tests for tools, permissions, plugins, MCP, or multi-agent flows. +- Contribute new skills, plugins, or provider compatibility improvements. +- Share real usage patterns that can be added to [`docs/SHOWCASE.md`](docs/SHOWCASE.md). + +## Development setup + +```bash +git clone https://github.com/HKUDS/OpenHarness.git +cd OpenHarness +uv sync --extra dev +``` + +If you want to work on the React terminal UI as well: + +```bash +cd frontend/terminal +npm ci +cd ../.. +``` + +## Local checks + +Run the same core checks that CI runs before opening a PR: + +```bash +uv run ruff check src tests scripts +uv run pytest -q +``` + +Frontend sanity check: + +```bash +cd frontend/terminal +npx tsc --noEmit +``` + +## Pull request expectations + +- Keep PRs scoped. Small, reviewable changes merge faster than broad rewrites. +- Include the problem, the change, and how you verified it. +- Add or update tests when behavior changes. +- Update docs when CLI flags, workflows, or compatibility claims change. +- Add a short entry under `Unreleased` in [`CHANGELOG.md`](CHANGELOG.md) for user-visible changes. +- If you are improving type coverage, feel free to run `uv run mypy src/openharness`, but it is not yet a required green check for the whole repo. + +## Documentation and community contributions + +Issue [#7](https://github.com/HKUDS/OpenHarness/issues/7) surfaced several high-value docs needs. Useful contributions in that area include: + +- README accuracy improvements and compatibility notes. +- Short, reproducible examples for common workflows. +- Showcase entries based on real usage rather than generic marketing claims. +- Contribution and maintenance docs that make the repo easier to navigate. + +## Reporting bugs and proposing features + +- Use the GitHub issue templates when possible. +- Include environment details, exact commands, and error output for bugs. +- For features, explain the concrete workflow gap and expected behavior. +- If the request is mostly documentation or maintenance related, say that explicitly so it can be scoped as a docs PR. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..06c5e27 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 OpenHarness Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c99de34 --- /dev/null +++ b/README.md @@ -0,0 +1,879 @@ +

+ OpenHarness +    + ohmo +
+ oh — OpenHarness & ohmo +

+ +

+ English · + 简体中文 +

+ +**OpenHarness** delivers core lightweight agent infrastructure: tool-use, skills, memory, and multi-agent coordination. + +**ohmo** is a personal AI agent built on OpenHarness — not another chatbot, but an assistant that actually works for you over long sessions. Chat with ohmo in Feishu / Slack / Telegram / Discord, and it forks branches, writes code, runs tests, and opens PRs on its own. ohmo runs on your existing Claude Code or Codex subscription — no extra API key needed. + +**Join the community**: contribute **Harness** for open agent development. + +

+ Quick Start + Architecture + Tools + Tests + License +

+ +

+ Python + React + Pytest + E2E + Output + CI + Feishu + WeChat +

+ +One Command (**oh**) to Launch **OpenHarness** and Unlock All Agent Harnesses. + +Supports CLI agent integration including OpenClaw, nanobot, Cursor, and more. + +

+ OpenHarness Terminal Demo +

+ +--- +## ✨ OpenHarness's Key Harness Features + + + + + + + + + +
+ +

🔄 Agent Loop

+ +
+ Engine +
+ + + +

• Streaming Tool-Call Cycle

+

• API Retry with Exponential Backoff

+

• Parallel Tool Execution

+

• Token Counting & Cost Tracking

+ +
+ +

🔧 Harness Toolkit

+ +
+ Toolkit +
+ + + +

• 43 Tools (File, Shell, Search, Web, MCP)

+

• On-Demand Skill Loading (.md)

+

• Plugin Ecosystem (Skills + Hooks + Agents)

+

• Compatible with anthropics/skills & plugins

+ +
+ +

🧠 Context & Memory

+ +
+ Context +
+ + + +

• CLAUDE.md Discovery & Injection

+

• Context Compression (Auto-Compact)

+

• MEMORY.md Persistent Memory

+

• Session Resume & History

+ +
+ +

🛡️ Governance

+ +
+ Governance +
+ + + +

• Multi-Level Permission Modes

+

• Path-Level & Command Rules

+

• PreToolUse / PostToolUse Hooks

+

• Interactive Approval Dialogs

+ +
+ +

🤝 Swarm Coordination

+ +
+ Swarm +
+ + + +

• Subagent Spawning & Delegation

+

• Team Registry & Task Management

+

• Background Task Lifecycle

+

ClawTeam Integration (Roadmap)

+ +
+ +--- + +## 🤔 What is an Agent Harness? + +An **Agent Harness** is the complete infrastructure that wraps around an LLM to make it a functional agent. The model provides intelligence; the harness provides **hands, eyes, memory, and safety boundaries**. + +

+ Harness = Tools + Knowledge + Observation + Action + Permissions +

+ +OpenHarness is an open-source Python implementation designed for **researchers, builders, and the community**: + +- **Understand** how production AI agents work under the hood +- **Experiment** with cutting-edge tools, skills, and agent coordination patterns +- **Extend** the harness with custom plugins, providers, and domain knowledge +- **Build** specialized agents on top of proven architecture + +--- + +## 📰 What's New + +- **Unreleased** 🔍 **Dry-run safe preview**: + - `oh --dry-run` previews resolved runtime settings, auth state, skills, commands, tools, and configured MCP servers without executing the model, tools, or subagents. + - Dry-run now reports a `ready` / `warning` / `blocked` readiness verdict with concrete next-step suggestions such as fixing auth, fixing MCP config, or running the prompt directly. + - Prompt previews include likely matching skills and tools, while slash-command previews show whether the command is mostly read-only or stateful. +- **2026-04-18** ⚙️ **v0.1.7** — Packaging & TUI polish: + - Install script now links `oh`, `ohmo`, and `openharness` into `~/.local/bin` instead of prepending the virtualenv `bin` directory to `PATH`, which avoids clobbering Conda-managed shells. + - React TUI now supports `Shift+Enter` to insert a newline while keeping plain `Enter` as submit. + - Busy-state animation in the React TUI is quieter and less error-prone on Windows terminals, with conservative spinner frames and reduced flashing. +- **2026-04-10** 🧠 **v0.1.6** — Auto-Compaction & Markdown TUI: + - Auto-Compaction preserves task state and channel logs across context compression — agents can run multi-day sessions without manual compact/clear + - Subprocess teammates run in headless worker mode; agent team creation stabilized + - Assistant messages now render full Markdown in the React TUI + - `ohmo` gains channel slash commands and multimodal attachment support +- **2026-04-08** 🔌 **v0.1.5** — MCP HTTP transport & Swarm polling: + - MCP protocol adds HTTP transport, auto-reconnect on disconnect, and tool-only server compatibility + - JSON Schema types inferred for MCP tool inputs — no manual type mapping needed + - `ohmo` channels support file attachments and multimodal gateway messages + - Subprocess agents are now pollable in real runs; permission modals serialized to prevent input swallowing +- **2026-04-08** 🌙 **v0.1.4** — Multi-provider auth & Moonshot/Kimi: + - Native Moonshot/Kimi provider with `reasoning_content` support for thinking models + - Auth overhaul: fixed provider-switching key mismatch, `OPENAI_BASE_URL` env override, profile-scoped credential priority + - MCP gracefully handles disconnected servers in `call_tool` / `read_resource` + - Security: built-in sensitive-path protection in PermissionChecker, hardened `web_fetch` URL validation + - Stability: EIO crash recovery in Ink TUI, `--debug` logging, Windows cmd flash fix +- **2026-04-06** 🚀 **v0.1.2** — Unified setup flows and `ohmo` personal-agent app: + - `oh setup` now guides provider selection as workflows instead of exposing raw auth/provider internals + - Compatible API setup is now profile-scoped, so Anthropic/OpenAI-compatible endpoints can keep separate keys + - `ohmo` ships as a packaged app with `~/.ohmo` workspace, gateway, bootstrap prompts, and channel config flow +- **2026-04-01** 🎨 **v0.1.0** — Initial **OpenHarness** open-source release featuring complete Harness architecture: + +

+ Start here: + Quick Start · + Provider Compatibility · + Showcase · + Contributing · + Changelog +

+ +--- + +## 🚀 Quick Start + +### 1. Install + +#### Linux / macOS / WSL + +```bash +# One-click install +curl -fsSL https://raw.githubusercontent.com/HKUDS/OpenHarness/main/scripts/install.sh | bash + +# Or via pip +pip install openharness-ai +``` + +#### Windows (Native) + +```powershell +# One-click install (PowerShell) +iex (Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/HKUDS/OpenHarness/main/scripts/install.ps1') + +# Or via pip +pip install openharness-ai +``` + +**Note**: Windows support is now native. In PowerShell, use `openh` instead of `oh` because `oh` can resolve to the built-in `Out-Host` alias. + +### 2. Configure + +```bash +oh setup # interactive wizard — pick a provider, authenticate, done +# On Windows PowerShell, use: openh setup +``` + +Supports **Claude / OpenAI / Copilot / Codex / Moonshot(Kimi) / GLM / MiniMax / NVIDIA NIM** and any compatible endpoint. + +### 3. Run + +```bash +oh +# On Windows PowerShell, use: openh +``` + +

+ OpenHarness Landing Screen +

+ +### 4. Set up ohmo (Personal Agent) + +Want an AI agent that works for you from Feishu / Slack / Telegram / Discord? + +```bash +ohmo init # initialize ~/.ohmo workspace +ohmo config # configure channels and provider +ohmo gateway start # start the gateway — ohmo is now live in your chat app +``` + +ohmo runs on your existing **Claude Code subscription** or **Codex subscription** — no extra API key needed. + +### Non-Interactive Mode (Pipes & Scripts) + +```bash +# Single prompt → stdout +oh -p "Explain this codebase" + +# JSON output for programmatic use +oh -p "List all functions in main.py" --output-format json + +# Stream JSON events in real-time +oh -p "Fix the bug" --output-format stream-json +``` + +### Dry Run (Safe Preview) + +Use `--dry-run` when you want to inspect what OpenHarness would use before any live execution starts. + +```bash +# Preview an interactive session setup +oh --dry-run + +# Preview one prompt without executing the model or tools +oh --dry-run -p "Review this bug fix and grep for failing tests" + +# Preview a slash command path +oh --dry-run -p "/plugin list" + +# Get structured output for scripts or channels +oh --dry-run -p "Explain this repository" --output-format json +``` + +Dry-run is intentionally static: + +- It does **not** call the model +- It does **not** execute tools or spawn subagents +- It does **not** connect to MCP servers +- It **does** resolve settings, auth status, prompt assembly, skills, commands, tools, and obvious MCP config problems + +Readiness levels: + +- `ready`: configuration looks usable; the next suggested action is usually to run the prompt directly +- `warning`: OpenHarness can resolve the session, but something important still looks wrong, such as broken MCP config or missing auth for later model work +- `blocked`: the requested path will not run successfully as-is, for example an unknown slash command or a prompt that cannot resolve a runtime client + +`next actions` in the dry-run output tell you the shortest fix or follow-up step, such as: + +- run `oh auth login` +- fix or disable broken MCP configuration +- run the prompt directly with `oh -p "..."` or open the interactive UI with `oh` + +## 🔌 Provider Compatibility + +OpenHarness treats providers as **workflows** backed by named profiles. In day-to-day use, prefer: + +```bash +oh setup +oh provider list +oh provider use +``` + +### Built-in Workflows + +| Workflow | What it is | Typical backends | +|----------|------------|------------------| +| **Anthropic-Compatible API** | Anthropic-style request format | Claude official, Kimi, GLM, MiniMax, internal Anthropic-compatible gateways | +| **Claude Subscription** | Claude CLI subscription bridge | Local `~/.claude/.credentials.json` | +| **OpenAI-Compatible API** | OpenAI-style request format | OpenAI official, OpenRouter, DashScope, DeepSeek, SiliconFlow, Groq, Ollama, GitHub Models | +| **Codex Subscription** | Codex CLI subscription bridge | Local `~/.codex/auth.json` | +| **GitHub Copilot** | Copilot OAuth workflow | GitHub Copilot device-flow login | + +### Compatible API Families + +#### Anthropic-Compatible API + +Typical examples: + +| Backend | Base URL | Example models | +|---------|----------|----------------| +| **Claude official** | `https://api.anthropic.com` | `claude-sonnet-4-6`, `claude-opus-4-6` | +| **Moonshot / Kimi** | `https://api.moonshot.cn/anthropic` | `kimi-k2.5` | +| **Zhipu / GLM** | custom Anthropic-compatible endpoint | `glm-4.5` | +| **MiniMax** | custom Anthropic-compatible endpoint | `minimax-m1` | + +#### OpenAI-Compatible API + +Any provider implementing the OpenAI `/v1/chat/completions` style API works: + +| Backend | Base URL | Example models | +|---------|----------|----------------| +| **OpenAI** | `https://api.openai.com/v1` | `gpt-5.4`, `gpt-4.1` | +| **OpenRouter** | `https://openrouter.ai/api/v1` | provider-specific | +| **Alibaba DashScope** | `https://dashscope.aliyuncs.com/compatible-mode/v1` | `qwen3.5-flash`, `qwen3-max`, `deepseek-r1` | +| **DeepSeek** | `https://api.deepseek.com` | `deepseek-chat`, `deepseek-reasoner` | +| **GitHub Models** | `https://models.inference.ai.azure.com` | `gpt-4o`, `Meta-Llama-3.1-405B-Instruct` | +| **SiliconFlow** | `https://api.siliconflow.cn/v1` | `deepseek-ai/DeepSeek-V3` | +| **NVIDIA NIM** | `https://integrate.api.nvidia.com/v1` | `openai/gpt-oss-120b`, `nvidia/llama-3.3-nemotron-super-49b-v1` | +| **Google Gemini** | `https://generativelanguage.googleapis.com/v1beta/openai` | `gemini-2.5-flash`, `gemini-2.5-pro` | +| **Groq** | `https://api.groq.com/openai/v1` | `llama-3.3-70b-versatile` | +| **Ollama (local)** | `http://localhost:11434/v1` | any local model | + +### Advanced Profile Management + +```bash +# List saved workflows +oh provider list + +# Switch the active workflow +oh provider use codex + +# Add your own compatible endpoint +oh provider add my-endpoint \ + --label "My Endpoint" \ + --provider openai \ + --api-format openai \ + --auth-source openai_api_key \ + --model my-model \ + --base-url https://example.com/v1 +``` + +For custom compatible endpoints, OpenHarness can bind credentials per profile instead of forcing every Anthropic-compatible or OpenAI-compatible backend to share the same API key. + +### Ollama (Local Models) + +Run local models through Ollama's OpenAI-compatible endpoint: + +```bash +# Add an Ollama provider profile +oh provider add ollama \ + --label "Ollama" \ + --provider Ollama \ + --api-format openai \ + --auth-source openai_api_key \ + --model glm-4.7-flash:q8_0 \ + --base-url http://localhost:11434/v1 +``` +``` +Saved provider profile: ollama +``` + +```bash +# Activate and verify +oh provider use ollama +``` +``` +Activated provider profile: ollama +``` + +```bash +oh provider list +``` +``` + claude-api: Anthropic-Compatible API [ready] + ... + moonshot: Moonshot (Kimi) [missing auth] + auth=moonshot_api_key model=kimi-k2.5 base_url=https://api.moonshot.cn/v1 +* ollama: Ollama [ready] + auth=openai_api_key model=glm-4.7-flash:q8_0 base_url=http://localhost:11434/v1 +``` + +### GitHub Copilot Format (`--api-format copilot`) + +Use your existing GitHub Copilot subscription as the LLM backend. Authentication uses GitHub's OAuth device flow — no API keys needed. + +```bash +# One-time login (opens browser for GitHub authorization) +oh auth copilot-login + +# Then launch with Copilot as the provider +uv run oh --api-format copilot + +# Or via environment variable +export OPENHARNESS_API_FORMAT=copilot +uv run oh + +# Check auth status +oh auth status + +# Remove stored credentials +oh auth copilot-logout +``` + +| Feature | Details | +|---------|---------| +| **Auth method** | GitHub OAuth device flow (no API key needed) | +| **Token management** | Automatic refresh of short-lived session tokens | +| **Enterprise** | Supports GitHub Enterprise via `--github-domain` flag | +| **Models** | Uses Copilot's default model selection | +| **API** | OpenAI-compatible chat completions under the hood | + +--- + +## 🏗️ Harness Architecture + +OpenHarness implements the core Agent Harness pattern with 10 subsystems: + +``` +openharness/ + engine/ # 🧠 Agent Loop — query → stream → tool-call → loop + tools/ # 🔧 43 Tools — file I/O, shell, search, web, MCP + skills/ # 📚 Knowledge — on-demand skill loading (.md files) + plugins/ # 🔌 Extensions — commands, hooks, agents, MCP servers + permissions/ # 🛡️ Safety — multi-level modes, path rules, command deny + hooks/ # ⚡ Lifecycle — PreToolUse/PostToolUse event hooks + commands/ # 💬 54 Commands — /help, /commit, /plan, /resume, ... + mcp/ # 🌐 MCP — Model Context Protocol client + memory/ # 🧠 Memory — persistent cross-session knowledge + tasks/ # 📋 Tasks — background task management + coordinator/ # 🤝 Multi-Agent — subagent spawning, team coordination + prompts/ # 📝 Context — system prompt assembly, CLAUDE.md, skills + config/ # ⚙️ Settings — multi-layer config, migrations + ui/ # 🖥️ React TUI — backend protocol + frontend +``` + +### The Agent Loop + +The heart of the harness. One loop, endlessly composable: + +```python +while True: + response = await api.stream(messages, tools) + + if response.stop_reason != "tool_use": + break # Model is done + + for tool_call in response.tool_uses: + # Permission check → Hook → Execute → Hook → Result + result = await harness.execute_tool(tool_call) + + messages.append(tool_results) + # Loop continues — model sees results, decides next action +``` + +The model decides **what** to do. The harness handles **how** — safely, efficiently, with full observability. + +### Harness Flow + +```mermaid +flowchart LR + U[User Prompt] --> C[CLI or React TUI] + C --> R[RuntimeBundle] + R --> Q[QueryEngine] + Q --> A[Anthropic-compatible API Client] + A -->|tool_use| T[Tool Registry] + T --> P[Permissions + Hooks] + P --> X[Files Shell Web MCP Tasks] + X --> Q +``` + +--- + +## ✨ Features + +### 🔧 Tools (43+) + +| Category | Tools | Description | +|----------|-------|-------------| +| **File I/O** | Bash, Read, Write, Edit, Glob, Grep | Core file operations with permission checks | +| **Search** | WebFetch, WebSearch, ToolSearch, LSP | Web and code search capabilities | +| **Notebook** | NotebookEdit | Jupyter notebook cell editing | +| **Agent** | Agent, SendMessage, TeamCreate/Delete | Subagent spawning and coordination | +| **Task** | TaskCreate/Get/List/Update/Stop/Output | Background task management | +| **MCP** | MCPTool, ListMcpResources, ReadMcpResource | Model Context Protocol integration | +| **Mode** | EnterPlanMode, ExitPlanMode, Worktree | Workflow mode switching | +| **Schedule** | CronCreate/List/Delete, RemoteTrigger | Scheduled and remote execution | +| **Meta** | Skill, Config, Brief, Sleep, AskUser | Knowledge loading, configuration, interaction | + +Every tool has: +- **Pydantic input validation** — structured, type-safe inputs +- **Self-describing JSON Schema** — models understand tools automatically +- **Permission integration** — checked before every execution +- **Hook support** — PreToolUse/PostToolUse lifecycle events + +### 📚 Skills System + +Skills are **on-demand knowledge** — loaded only when the model needs them: + +``` +Available Skills: +- commit: Create clean, well-structured git commits +- review: Review code for bugs, security issues, and quality +- debug: Diagnose and fix bugs systematically +- plan: Design an implementation plan before coding +- test: Write and run tests for code +- simplify: Refactor code to be simpler and more maintainable +- pdf: PDF processing with pypdf (from anthropics/skills) +- xlsx: Excel operations (from anthropics/skills) +- ... 40+ more +``` + +Skills can live in bundled, user, ohmo, project, or plugin locations. User-level skills are loaded from: + +```text +~/.openharness/skills//SKILL.md +~/.claude/skills//SKILL.md +~/.agents/skills//SKILL.md +``` + +Project-level skills are enabled by default and are discovered from the current working directory up to the git root: + +```text +/.openharness/skills//SKILL.md +/.agents/skills//SKILL.md +/.claude/skills//SKILL.md +``` + +Disable project skills for untrusted repositories with: + +```bash +oh config set allow_project_skills false +``` + +Use `/skills` to list loaded skills with their source and path. User-invocable skills can be run directly as slash commands, for example `/deploy staging`. + +**Compatible with [anthropics/skills](https://github.com/anthropics/skills)** — use the `SKILL.md` directory layout above. + +### 🌐 Web search and proxy settings + +Built-in `web_search` uses DuckDuckGo HTML search by default. In regions where that endpoint is unreachable, point OpenHarness at a trusted public HTML search endpoint or your own SearXNG instance: + +```bash +export OPENHARNESS_WEB_SEARCH_URL="https://your-searxng.example/search" +``` + +`web_search` and `web_fetch` keep `trust_env=False` for SSRF safety, so they do not automatically inherit `HTTP_PROXY` / `HTTPS_PROXY`. If you need a proxy, opt in with an OpenHarness-specific variable: + +```bash +export OPENHARNESS_WEB_PROXY="http://127.0.0.1:7890" +``` + +The proxy URL must be HTTP/HTTPS and cannot contain embedded credentials. + +### 🔌 Plugin System + +**Compatible with [claude-code plugins](https://github.com/anthropics/claude-code/tree/main/plugins)**. Tested with 12 official plugins: + +| Plugin | Type | What it does | +|--------|------|-------------| +| `commit-commands` | Commands | Git commit, push, PR workflows | +| `security-guidance` | Hooks | Security warnings on file edits | +| `hookify` | Commands + Agents | Create custom behavior hooks | +| `feature-dev` | Commands | Feature development workflow | +| `code-review` | Agents | Multi-agent PR review | +| `pr-review-toolkit` | Agents | Specialized PR review agents | + +```bash +# Manage plugins +oh plugin list +oh plugin install +oh plugin enable +``` + +### 🤝 Ecosystem Workflows + +OpenHarness is useful as a lightweight harness layer around Claude-style tooling conventions: + +- **OpenClaw-oriented workflows** can reuse Markdown-first knowledge and command-driven collaboration patterns. +- **Claude-style plugins and skills** stay portable because OpenHarness keeps those formats familiar. +- **ClawTeam-style multi-agent work** maps well onto the built-in team, task, and background execution primitives. + +For concrete usage ideas instead of generic claims, see [`docs/SHOWCASE.md`](docs/SHOWCASE.md). + +### 🛡️ Permissions + +Multi-level safety with fine-grained control: + +| Mode | Behavior | Use Case | +|------|----------|----------| +| **Default** | Ask before write/execute | Daily development | +| **Auto** | Allow everything | Sandboxed environments | +| **Plan Mode** | Block all writes | Large refactors, review first | + +**Path-level rules** in `settings.json`: +```json +{ + "permission": { + "mode": "default", + "path_rules": [{"pattern": "/etc/*", "allow": false}], + "denied_commands": ["rm -rf /", "DROP TABLE *"] + } +} +``` + +### 🖥️ Terminal UI + +React/Ink TUI with full interactive experience: + +- **Command picker**: Type `/` → arrow keys to select → Enter +- **Permission dialog**: Interactive y/n with tool details +- **Mode switcher**: `/permissions` → select from list +- **Session resume**: `/resume` → pick from history +- **Animated spinner**: Real-time feedback during tool execution +- **Keyboard shortcuts**: Shown at the bottom, context-aware + +### 📡 CLI + +``` +oh [OPTIONS] COMMAND [ARGS] + +Session: -c/--continue, -r/--resume, -n/--name +Model: -m/--model, --effort, --max-turns +Output: -p/--print, --output-format text|json|stream-json +Permissions: --permission-mode, --dangerously-skip-permissions +Context: -s/--system-prompt, --append-system-prompt, --settings +Advanced: -d/--debug, --mcp-config, --bare + +Subcommands: oh setup | oh provider | oh auth | oh mcp | oh plugin +``` + +### 🧑‍💼 ohmo Personal Agent + +`ohmo` is a personal-agent app built on top of OpenHarness. It is packaged alongside `oh`, with its own workspace and gateway: + +```bash +# Initialize personal workspace +ohmo init + +# Configure gateway channels and pick a provider profile +ohmo config + +# Run the personal agent +ohmo + +# Run the gateway in foreground +ohmo gateway run + +# Check or restart the gateway +ohmo gateway status +ohmo gateway restart +``` + +Key concepts: + +- `~/.ohmo/` + - personal workspace root +- `soul.md` + - long-term agent personality and behavior +- `identity.md` + - who `ohmo` is +- `user.md` + - user profile and preferences +- `BOOTSTRAP.md` + - first-run landing ritual +- `memory/` + - personal memory +- `gateway.json` + - selected provider profile and channel configuration + +`ohmo config` uses the same workflow language as `oh setup`, so you can point the personal-agent gateway at: + +- `Anthropic-Compatible API` +- `Claude Subscription` +- `OpenAI-Compatible API` +- `Codex Subscription` +- `GitHub Copilot` + +`ohmo init` creates the home workspace once. After that, use `ohmo config` to update provider and channel settings; if the gateway is already running, the config flow can restart it for you. + +Currently `ohmo init` / `ohmo config` can guide channel setup for: + +- Telegram +- Slack +- Discord +- Feishu + +--- + +## 📊 Test Results + +| Suite | Tests | Status | +|-------|-------|--------| +| Unit + Integration | 114 | ✅ All passing | +| CLI Flags E2E | 6 | ✅ Real model calls | +| Harness Features E2E | 9 | ✅ Retry, skills, parallel, permissions | +| React TUI E2E | 3 | ✅ Welcome, conversation, status | +| TUI Interactions E2E | 4 | ✅ Commands, permissions, shortcuts | +| Real Skills + Plugins | 12 | ✅ anthropics/skills + claude-code/plugins | + +```bash +# Run all tests +uv run pytest -q # 114 unit/integration +python scripts/test_harness_features.py # Harness E2E +python scripts/test_real_skills_plugins.py # Real plugins E2E +``` + +--- + +## 🔧 Extending OpenHarness + +### Add a Custom Tool + +```python +from pydantic import BaseModel, Field +from openharness.tools.base import BaseTool, ToolExecutionContext, ToolResult + +class MyToolInput(BaseModel): + query: str = Field(description="Search query") + +class MyTool(BaseTool): + name = "my_tool" + description = "Does something useful" + input_model = MyToolInput + + async def execute(self, arguments: MyToolInput, context: ToolExecutionContext) -> ToolResult: + return ToolResult(output=f"Result for: {arguments.query}") +``` + +### Add a Custom Skill + +Create `~/.openharness/skills/my-skill.md`: + +```markdown +--- +name: my-skill +description: Expert guidance for my specific domain +--- + +# My Skill + +## When to use +Use when the user asks about [your domain]. + +## Workflow +1. Step one +2. Step two +... +``` + +### Add a Plugin + +Create `.openharness/plugins/my-plugin/.claude-plugin/plugin.json`: + +```json +{ + "name": "my-plugin", + "version": "1.0.0", + "description": "My custom plugin" +} +``` + +Add commands in `commands/*.md`, hooks in `hooks/hooks.json`, agents in `agents/*.md`. + +--- + +## 🌍 Showcase + +OpenHarness is most useful when treated as a small, inspectable harness you can adapt to a real workflow: + +- **Repo coding assistant** for reading code, patching files, and running checks locally. +- **Headless scripting tool** for `json` and `stream-json` output in automation flows. +- **Plugin and skill testbed** for experimenting with Claude-style extensions. +- **Multi-agent prototype harness** for task delegation and background execution. +- **Provider comparison sandbox** across Anthropic-compatible backends. + +See [`docs/SHOWCASE.md`](docs/SHOWCASE.md) for short, reproducible examples. + +--- + +## 🤝 Contributing + +OpenHarness is a **community-driven research project**. We welcome contributions in: + +| Area | Examples | +|------|---------| +| **Tools** | New tool implementations for specific domains | +| **Skills** | Domain knowledge `.md` files (finance, science, DevOps...) | +| **Plugins** | Workflow plugins with commands, hooks, agents | +| **Providers** | Support for more LLM backends (OpenAI, Ollama, etc.) | +| **Multi-Agent** | Coordination protocols, team patterns | +| **Testing** | E2E scenarios, edge cases, benchmarks | +| **Documentation** | Architecture guides, tutorials, translations | + +```bash +# Development setup +git clone https://github.com/HKUDS/OpenHarness.git +cd OpenHarness +uv sync --extra dev +uv run pytest -q # Verify everything works +``` + +Useful contributor entry points: + +- [`CONTRIBUTING.md`](CONTRIBUTING.md) for setup, checks, and PR expectations +- [`CHANGELOG.md`](CHANGELOG.md) for user-visible changes +- [`docs/SHOWCASE.md`](docs/SHOWCASE.md) for real-world usage patterns worth documenting + +--- + +## 🔧 Troubleshooting + +### Backspace key in macOS Terminal.app + +OpenHarness handles both common terminal delete sequences, including the raw `DEL` byte (`0x7f`) that macOS Terminal.app sends for Backspace. If Backspace inserts spaces or visible control characters instead of deleting text, upgrade OpenHarness first. + +For older versions that do not include this fix, use a terminal that sends a standard Backspace sequence or adjust your terminal keyboard profile as a temporary workaround. + +--- + +## 📄 License + +MIT — see [LICENSE](LICENSE). + +--- + +

+ OpenHarness +
+ Oh my Harness! +
+ The model is the agent. The code is the harness. +

+ + + +

+ Thanks for visiting ✨ OpenHarness!

+ Views +

diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..598b7e3 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`HKUDS/OpenHarness` +- 原始仓库:https://github.com/HKUDS/OpenHarness +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..58af296 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,417 @@ +# OpenHarness `oh` — OpenHarness 中文说明 + +

+ English · + 简体中文 +

+ +**OpenHarness** 是一个面向开源社区的 Agent Harness。它提供轻量、可扩展、可检查的 Agent 基础设施,包括: + +- Agent loop +- tools / skills / plugins +- memory / session resume +- permissions / hooks +- multi-agent coordination +- provider workflows +- React TUI +- `ohmo` personal-agent app + +--- + +## 最新更新 + +### Unreleased · Dry-run 安全预览 + +- 新增 `oh --dry-run`,可以在**不执行模型、不执行工具、不 spawn subagent** 的前提下,预览当前会话会使用的配置、skills、commands、tools 和 MCP 配置。 +- Dry-run 会给出 `ready / warning / blocked` 结论,并直接告诉你下一步该做什么,例如先修认证、先修 MCP 配置,或者可以直接运行。 +- 对普通 prompt,会给出可能命中的 skills / tools;对 slash command,会展示它更偏只读还是会改本地状态。 + +### 2026-04-06 · v0.1.2 + +- 新增统一配置入口 `oh setup` +- provider 配置从“auth -> provider -> model”收敛成 workflow 视角 +- Anthropic/OpenAI 兼容接口支持 profile 级凭据,不再强制共用一把全局 key +- 新增 `ohmo` personal-agent app +- `ohmo` 使用 `~/.ohmo` 作为 home workspace,支持 gateway、bootstrap prompts 和交互式 channel 配置 + +--- + +## 快速开始 + +### 一键安装 + +```bash +curl -fsSL https://raw.githubusercontent.com/HKUDS/OpenHarness/main/scripts/install.sh | bash +``` + +常用安装参数: + +- `--from-source`:从源码安装,适合贡献者 +- `--with-channels`:一并安装 IM channel 依赖 + +例如: + +```bash +curl -fsSL https://raw.githubusercontent.com/HKUDS/OpenHarness/main/scripts/install.sh | bash -s -- --from-source --with-channels +``` + +### 本地运行 + +```bash +git clone https://github.com/HKUDS/OpenHarness.git +cd OpenHarness +uv sync --extra dev +uv run oh +``` + +--- + +## 配置模型与 Provider + +现在最推荐的入口是: + +```bash +oh setup +``` + +`oh setup` 会按下面的顺序引导: + +1. 选择一个 workflow +2. 如果需要,完成认证 +3. 选择具体后端 preset +4. 确认模型 +5. 保存并激活 profile + +当前内置 workflow 包括: + +- `Anthropic-Compatible API` +- `Claude Subscription` +- `OpenAI-Compatible API` +- `Codex Subscription` +- `GitHub Copilot` + +### Anthropic-Compatible API + +适合这类后端: + +- Claude 官方 API +- Moonshot / Kimi +- Zhipu / GLM +- MiniMax +- 其他 Anthropic-compatible endpoint + +### OpenAI-Compatible API + +适合这类后端: + +- OpenAI 官方 API +- OpenRouter +- DashScope +- DeepSeek +- GitHub Models +- SiliconFlow +- Google Gemini +- Groq +- Ollama +- 其他 OpenAI-compatible endpoint + +### 常用命令 + +```bash +# 统一配置入口 +oh setup + +# 查看已有 workflow/profile +oh provider list + +# 切换当前 workflow +oh provider use codex + +# 查看认证状态 +oh auth status +``` + +### 高级:添加自定义兼容接口 + +如果内置 preset 不够,可以直接新增 profile: + +```bash +oh provider add my-endpoint \ + --label "My Endpoint" \ + --provider anthropic \ + --api-format anthropic \ + --auth-source anthropic_api_key \ + --model my-model \ + --base-url https://example.com/anthropic +``` + +这一版开始,兼容接口可以按 profile 绑定凭据。 +也就是说,`Kimi`、`GLM`、`MiniMax` 这类 Anthropic-compatible 后端,不需要再共用一把全局 `anthropic` key。 + +--- + +## 交互模式与 TUI + +运行: + +```bash +oh +``` + +你会得到 React/Ink TUI,支持: + +- `/` 命令选择器 +- 交互式权限确认 +- `/model` 模型切换 +- `/permissions` 权限模式切换 +- `/resume` 会话恢复 +- `/provider` workflow 选择 + +非交互模式也支持: + +```bash +oh -p "Explain this repository" +oh -p "List all functions in main.py" --output-format json +oh -p "Fix the bug" --output-format stream-json +``` + +### Dry-run 安全预览 + +如果你想先看 OpenHarness **会怎么跑**,但又不想真的执行模型或工具,可以用: + +```bash +# 预览交互会话本身 +oh --dry-run + +# 预览一个普通 prompt +oh --dry-run -p "Review this bug fix and grep for failing tests" + +# 预览 slash command +oh --dry-run -p "/plugin list" + +# 输出结构化 JSON,方便脚本或 channel 使用 +oh --dry-run -p "Explain this repository" --output-format json +``` + +Dry-run 的边界是明确的: + +- **不会**调用模型 +- **不会**执行 tools +- **不会**启动 subagent +- **不会**连接 MCP server +- **会**解析 settings、auth 状态、system prompt、skills、commands、tools,以及明显错误的 MCP 配置 + +Readiness 结论说明: + +- `ready`:当前配置基本可直接运行 +- `warning`:能解析会话,但仍有重要问题需要先处理,比如 MCP 配置错误或后续模型调用缺认证 +- `blocked`:按当前状态直接运行会失败,比如 slash command 不存在,或者普通 prompt 无法解析 runtime client + +Dry-run 输出里的 `next actions` 会直接给出下一步建议,例如: + +- 先执行 `oh auth login` +- 先修或禁用坏掉的 MCP 配置 +- 直接运行 `oh -p "..."` 或进入 `oh` + +--- + +## Provider 兼容性概览 + +OpenHarness 现在把 provider 视为 **workflow + profile**,而不是只暴露底层协议名。 + +| Workflow | 说明 | +|----------|------| +| `Anthropic-Compatible API` | Anthropic 风格接口,适合 Claude/Kimi/GLM/MiniMax 等 | +| `Claude Subscription` | 复用本地 `~/.claude/.credentials.json` | +| `OpenAI-Compatible API` | OpenAI 风格接口,适合 OpenAI/OpenRouter/各种兼容网关 | +| `Codex Subscription` | 复用本地 `~/.codex/auth.json` | +| `GitHub Copilot` | GitHub Copilot OAuth workflow | + +日常推荐用法: + +```bash +oh setup +oh provider list +oh provider use +``` + +--- + +## `ohmo` Personal Agent + +`ohmo` 是基于 OpenHarness 的 personal-agent app,不是 core 的一个 mode。 + +### 初始化 + +```bash +ohmo init +``` + +这会创建: + +- `~/.ohmo/soul.md` +- `~/.ohmo/identity.md` +- `~/.ohmo/user.md` +- `~/.ohmo/BOOTSTRAP.md` +- `~/.ohmo/memory/` +- `~/.ohmo/gateway.json` + +其中: + +- `soul.md`:长期人格与行为原则 +- `identity.md`:`ohmo` 自己是谁 +- `user.md`:用户画像、偏好、关系信息 +- `BOOTSTRAP.md`:首轮 landing / onboarding ritual +- `memory/`:personal memory +- `gateway.json`:gateway 的 profile 和 channel 配置 + +### 配置 + +```bash +ohmo config +``` + +`ohmo config` 会用和 `oh setup` 一致的 workflow 语言来配置 gateway,例如: + +- `Anthropic-Compatible API` +- `Claude Subscription` +- `OpenAI-Compatible API` +- `Codex Subscription` +- `GitHub Copilot` + +目前 `ohmo init` / `ohmo config` 已支持引导式配置这些 channel: + +- Telegram +- Slack +- Discord +- Feishu + +如果 gateway 已经在运行,配置完成后也可以直接选择是否重启。 + +### 运行 + +```bash +# 运行 personal agent +ohmo + +# 前台运行 gateway +ohmo gateway run + +# 查看 gateway 状态 +ohmo gateway status + +# 重启 gateway +ohmo gateway restart +``` + +--- + +## OpenHarness 的核心能力 + +### Agent Loop + +- streaming tool-call cycle +- tool execution / observation / loop +- retry + exponential backoff +- token counting 与成本跟踪 + +### Tools / Skills / Plugins + +- 43+ tools +- Markdown skills 按需加载 +- 插件生态 +- 兼容 `anthropics/skills` +- 兼容 Claude-style plugins + +### Memory / Session + +- `CLAUDE.md` 自动发现与注入 +- `MEMORY.md` 持久记忆 +- session resume +- auto-compact + +### Governance + +- 多级 permission mode +- path rules +- denied commands +- hooks +- interactive approval + +### Multi-Agent + +- subagent spawning +- team registry +- task lifecycle +- background task execution + +--- + +## 常见命令 + +### `oh` + +```bash +oh setup +oh provider list +oh provider use codex +oh auth status +oh -p "Explain this codebase" +oh +``` + +### `ohmo` + +```bash +ohmo init +ohmo config +ohmo +ohmo gateway run +ohmo gateway status +ohmo gateway restart +``` + +--- + +## 测试 + +```bash +uv run pytest -q +python scripts/test_harness_features.py +python scripts/test_real_skills_plugins.py +``` + +--- + +## 贡献 + +欢迎贡献: + +- tools +- skills +- plugins +- providers +- multi-agent coordination +- tests +- 文档与中文翻译 + +开发环境: + +```bash +git clone https://github.com/HKUDS/OpenHarness.git +cd OpenHarness +uv sync --extra dev +uv run pytest -q +``` + +更多信息: + +- [贡献指南](CONTRIBUTING.md) +- [更新日志](CHANGELOG.md) +- [Showcase](docs/SHOWCASE.md) + +--- + +## License + +MIT,见 [LICENSE](LICENSE)。 diff --git a/RELEASE_NOTES_v0.1.8.md b/RELEASE_NOTES_v0.1.8.md new file mode 100644 index 0000000..6bdae30 --- /dev/null +++ b/RELEASE_NOTES_v0.1.8.md @@ -0,0 +1,66 @@ +# v0.1.8 — Providers, ohmo Feishu Groups, and Safer Remotes + +OpenHarness v0.1.8 is a stabilization and provider-expansion release. It adds more first-class provider workflows, improves ohmo's Feishu group experience, hardens remote-channel security, and fixes several Windows/MCP reliability issues. + +## Highlights + +- **New provider workflows** + - Added NVIDIA NIM as a built-in OpenAI-compatible provider using `NVIDIA_API_KEY`. + - Added ModelScope Inference API support. + - Added Qwen (DashScope), MiniMax, and Gemini provider profiles. + - Improved OpenAI-compatible API behavior, including explicit bearer authorization headers and `` block filtering for compatible streaming responses. + +- **ohmo Feishu group support** + - Added Feishu managed group creation flow for ohmo. + - Added gateway-scoped provider/model commands for chat-based operation. + - Improved group routing and mention policy so ohmo responds in shared Feishu groups only when explicitly addressed. + - Hardened Feishu attachment filename handling. + +- **Security and remote-channel hardening** + - Kept sensitive config/auth/provider/model/ship commands local-only by default in remote channels. + - Kept bridge commands local-only by default. + - Added coverage for bridge spawn blocking and remote gateway security behavior. + - Rejected path traversal names during plugin uninstall. + +- **Windows and shell reliability** + - Fixed Windows agent/subagent spawning by direct-executing teammate argv instead of shell-wrapping Python paths. + - Windows shell resolution now skips discovered `bash.exe` binaries that cannot actually run commands, falling back to PowerShell/cmd. + - Improved Windows gateway process lifecycle handling. + - Avoided shell execution when opening browsers on Windows. + +- **MCP, tools, and stability** + - MCP startup now isolates failed servers instead of aborting the whole OpenHarness startup. + - Fixed subprocess stderr pipe deadlocks in grep/glob/bash/session runner paths. + - Bounded large tool results in conversation history and improved compaction under large/vision contexts. + - Improved skill frontmatter parsing with YAML `safe_load` for bundled and user skills. + +- **TUI and UX fixes** + - Restored raw `DEL` backspace handling for macOS Terminal-style environments. + - Added better slash-command completion, markdown table rendering, spinner behavior, and escape interruption. + - Added image-to-text fallback support for text-only models and `--vision-model` override. + +## External contributors + +Thanks to the external contributors whose PRs are included in this release: + +- @Litianhui888 — MCP startup isolation (#237) +- @Hinotoi-agent — remote command security hardening (#232, #209, #208, #198, #197) +- @nsxdavid — Windows agent spawn fix (#231) +- @voidborne-d — bundled skill frontmatter parsing (#229) +- @Mcy0618 — image-to-text fallback and ModelScope support (#227, #224) +- @WANG-Guangxin — invalid regex fallback fix (#219) +- @glitch-ux — Windows browser auth safety, async coordinator draining, autopilot shell safety, hook events (#217, #200, #188, #170) +- @Escapingbug — Windows gateway lifecycle and Telegram proxy config fixes (#193, #192) +- @ZevGit — Qwen provider profile (#207) +- @yl-jiang — subprocess stderr deadlock fixes and OpenAI-compatible think-block filtering (#205, #174) +- @he-yufeng — TUI slash-command completion polish (#185) +- @powAu3 — raw DEL backspace fix (#182) +- @flobo3 — shell subprocess stdin default fix (#179) + +## Install + +```bash +pip install --upgrade openharness-ai==0.1.8 +``` + +Or run the installer from the repository docs. diff --git a/RELEASE_NOTES_v0.1.9.md b/RELEASE_NOTES_v0.1.9.md new file mode 100644 index 0000000..7d0e7e6 --- /dev/null +++ b/RELEASE_NOTES_v0.1.9.md @@ -0,0 +1,32 @@ +# v0.1.9 — Skill Workflows and Provider Key Updates + +OpenHarness v0.1.9 is a small follow-up release after v0.1.8 focused on making skills easier to create and invoke, plus fixing provider API key updates. + +## Highlights + +- **Bundled skill creator** + - Added a bundled `skill-creator` skill for creating, improving, and verifying OpenHarness/ohmo skills. + - This makes repeatable workflows easier to capture as first-class skills. + +- **User-invocable skill slash commands** + - Skills marked as user-invocable can now be triggered directly with slash commands. + - Slash-invoked skills support user arguments and can request a model override through skill metadata. + +- **Provider API key update fix** + - `oh setup` now lets users update the API key for an already-configured API-key provider profile. + - `oh provider edit --api-key ` can replace a saved profile key directly. + - `oh provider add ... --api-key ` can store a key while creating a provider profile. + +## Fixes + +- Fixed issue #238, where users could change a configured provider model but had no supported path to update the saved API key. + +## Install + +```bash +pip install --upgrade openharness-ai==0.1.9 +``` + +## Contributors + +This release is primarily a maintainer follow-up release. Thanks to the users and contributors who reported and validated the provider key update workflow, especially the reporter of #238. diff --git a/assets/architecture-comic.png b/assets/architecture-comic.png new file mode 100644 index 0000000000000000000000000000000000000000..cce631ec546ad30f7497c0246b4d65397f7029f0 GIT binary patch literal 1415458 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