limbo-code 0.1.13__tar.gz → 0.1.14__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {limbo_code-0.1.13 → limbo_code-0.1.14}/AGENTS.md +2 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/PKG-INFO +2 -2
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/agent.py +5 -0
- limbo_code-0.1.14/src/limbo/integrations/__init__.py +43 -0
- limbo_code-0.1.14/src/limbo/integrations/base.py +87 -0
- limbo_code-0.1.14/src/limbo/integrations/herdr.py +143 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/screens/main.py +33 -0
- limbo_code-0.1.14/tests/test_herdr.py +145 -0
- limbo_code-0.1.14/tests/test_integrations.py +60 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/.agents/skills/grill-with-docs/SKILL.md +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/.agents/skills/grill-with-docs/agents/openai.yaml +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/.agents/skills/improve-codebase-architecture/HTML-REPORT.md +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/.agents/skills/improve-codebase-architecture/SKILL.md +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/.agents/skills/improve-codebase-architecture/agents/openai.yaml +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/.agents/skills/tdd/SKILL.md +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/.agents/skills/tdd/agents/openai.yaml +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/.agents/skills/tdd/mocking.md +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/.agents/skills/tdd/tests.md +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/.github/workflows/publish.yml +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/.github/workflows/test.yml +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/.gitignore +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/CONTEXT.md +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/Makefile +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/README.md +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/design/confirm_view.html +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/design/limbo-ui-minimal.md +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/design/limbo-ui-redesign.md +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/design/prototype-minimal-confirm.png +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/design/prototype-minimal.html +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/design/prototype-minimal.png +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/design/prototype.html +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/design/prototype.png +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/assets/limbo-current-ui.png +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/assets/limbo-new-ui.png +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-dark/1_idle.svg +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-dark/2_thinking_tool_running.svg +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-dark/3_tool_success.svg +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-dark/4_tool_error.svg +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-dark/5_llm_error.svg +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-dark/6_edit_diff.svg +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-light/1_idle.svg +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-light/2_thinking_tool_running.svg +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-light/3_tool_success.svg +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-light/4_tool_error.svg +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-light/5_llm_error.svg +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/assets/walkthrough/limbo-light/6_edit_diff.svg +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/session-management.md +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/skills.md +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/docs/ui-redesign-proposal.md +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/pyproject.toml +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/scripts/check_contrast.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/scripts/gen_banner.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/scripts/ui_walkthrough.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/skills-lock.json +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/__init__.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/__main__.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/app.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/attachments.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/btw.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/compaction.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/config.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/goal.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/goal_driver.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/history.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/llm/__init__.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/llm/anthropic_client.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/llm/catalog.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/llm/client.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/llm/factory.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/llm/openai_client.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/llm/responses_client.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/llm/retry.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/llm/scaffold.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/llm/sse.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/llm/usage.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/model_switch.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/models.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/prompt.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/sessions.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/skills.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/steer.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/tools/__init__.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/tools/base.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/tools/bash.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/tools/edit.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/tools/find.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/tools/grep.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/tools/ignore.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/tools/ls.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/tools/mutation_queue.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/tools/read.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/tools/registry.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/tools/write.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/trace.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/__init__.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/app.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/app.tcss +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/banner.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/clipboard.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/commands.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/contrast.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/screens/__init__.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/screens/btw.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/screens/game2048.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/screens/model_picker.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/screens/session_picker.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/screens/verify_picker.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/syntax.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/theme.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/widgets/__init__.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/widgets/chat.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/widgets/command_menu.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/widgets/input.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/widgets/status_bar.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/ui/widgets/tool_card.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/src/limbo/user_paths.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/conftest.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_agent.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_anthropic_client.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_attachments.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_btw.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_catalog.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_cli.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_clipboard.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_compaction.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_config.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_goal.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_goal_driver.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_history.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_integration.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_interrupt.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_llm_client.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_llm_scaffold.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_model_switch.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_models.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_prompt.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_responses_client.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_retry.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_sessions.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_skills.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_sse.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_steer.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_trace.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_usage.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/test_user_paths.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/tools/test_base.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/tools/test_bash.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/tools/test_edit.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/tools/test_find.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/tools/test_grep.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/tools/test_ignore.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/tools/test_ls.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/tools/test_mutation_queue.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/tools/test_read.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/tools/test_registry.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/tools/test_write.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/__snapshots__/test_snapshots/test_snapshot_idle.raw +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/__snapshots__/test_snapshots/test_snapshot_session_picker.raw +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/__snapshots__/test_snapshots/test_snapshot_thinking_and_running_tool.raw +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/__snapshots__/test_snapshots/test_snapshot_tool_error_and_error_message.raw +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_app_smoke.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_btw_ui.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_command_menu.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_commands.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_compact_ui.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_contrast.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_copy.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_game2048.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_goal_ui.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_input_attachments.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_input_history.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_input_paste.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_interrupt_ui.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_main_screen.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_model_picker.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_scroll_follow.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_sessions_ui.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_skills_ui.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_snapshots.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_startup_art.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_steer_ui.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_theme.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/tests/ui/test_widgets.py +0 -0
- {limbo_code-0.1.13 → limbo_code-0.1.14}/uv.lock +0 -0
|
@@ -33,6 +33,8 @@ src/limbo/
|
|
|
33
33
|
├── goal_driver.py # /goal orchestrator (frontend-agnostic, non-UI): turn → verify → next round
|
|
34
34
|
├── sessions.py # Session JSONL save/load/list/export
|
|
35
35
|
├── trace.py # Append-only JSONL run log (sessions/traces/)
|
|
36
|
+
├── integrations/ # External mux/orchestrator lifecycle reporters: base.py (protocol
|
|
37
|
+
│ # + CompositeReporter + AgentState), herdr.py; create_reporters() fans out
|
|
36
38
|
├── skills.py # SKILL.md discovery (user + project dirs)
|
|
37
39
|
├── user_paths.py # Fence grants from paths in user messages
|
|
38
40
|
├── llm/ # catalog.py (provider/model specs), factory.py (dialect→client),
|
|
@@ -344,6 +344,11 @@ class Agent:
|
|
|
344
344
|
"""Stable id of the current session (the session file stem)."""
|
|
345
345
|
return self._meta.id
|
|
346
346
|
|
|
347
|
+
@property
|
|
348
|
+
def session_path(self) -> Path:
|
|
349
|
+
"""Path of the current session JSONL file."""
|
|
350
|
+
return self._session_file
|
|
351
|
+
|
|
347
352
|
@property
|
|
348
353
|
def session_meta(self) -> SessionMeta:
|
|
349
354
|
"""Metadata of the current session (title filled in on first save)."""
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Integrations with external terminal multiplexers / agent orchestrators.
|
|
2
|
+
|
|
3
|
+
Each integration lives in its own module (e.g. ``herdr``) and implements
|
|
4
|
+
the ``IntegrationReporter`` protocol from ``base``. Its ``from_env()``
|
|
5
|
+
returns ``None`` when Limbo is not running inside that tool, so the
|
|
6
|
+
integration is a strict no-op elsewhere. ``create_reporters()`` collects
|
|
7
|
+
every active integration into one ``CompositeReporter`` — the only object
|
|
8
|
+
call sites ever touch.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
|
|
16
|
+
from limbo.integrations.base import (
|
|
17
|
+
AgentState,
|
|
18
|
+
CompositeReporter,
|
|
19
|
+
IntegrationReporter,
|
|
20
|
+
)
|
|
21
|
+
from limbo.integrations.herdr import HerdrReporter
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"AgentState",
|
|
25
|
+
"CompositeReporter",
|
|
26
|
+
"HerdrReporter",
|
|
27
|
+
"IntegrationReporter",
|
|
28
|
+
"create_reporters",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def create_reporters(env: Mapping[str, str] | None = None) -> CompositeReporter:
|
|
33
|
+
"""Detect active integrations from the environment.
|
|
34
|
+
|
|
35
|
+
Register new integrations here: append ``XReporter.from_env(env)`` when
|
|
36
|
+
it returns a reporter. Order is irrelevant; each reporter is
|
|
37
|
+
independent and fire-and-forget.
|
|
38
|
+
"""
|
|
39
|
+
env = os.environ if env is None else env
|
|
40
|
+
reporters: list[IntegrationReporter] = []
|
|
41
|
+
if (herdr := HerdrReporter.from_env(env)) is not None:
|
|
42
|
+
reporters.append(herdr)
|
|
43
|
+
return CompositeReporter(reporters)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Shared contract for terminal multiplexer / agent orchestrator integrations.
|
|
2
|
+
|
|
3
|
+
Every integration (Herdr, future *mux tools, ...) implements
|
|
4
|
+
``IntegrationReporter``: it receives semantic lifecycle state
|
|
5
|
+
(``working``/``idle``/``blocked``) plus session identity, and maps that onto
|
|
6
|
+
whatever the specific tool understands. Call sites (UI, agent loop) only
|
|
7
|
+
ever talk to ``CompositeReporter`` — adding a new integration means writing
|
|
8
|
+
one module here and registering it in ``create_reporters()``, with zero
|
|
9
|
+
changes at call sites.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Iterable
|
|
15
|
+
from typing import Literal, Protocol
|
|
16
|
+
|
|
17
|
+
# Semantic lifecycle states every integration must be able to represent.
|
|
18
|
+
# ``blocked`` means the agent is waiting on a user decision.
|
|
19
|
+
AgentState = Literal["working", "idle", "blocked"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class IntegrationReporter(Protocol):
|
|
23
|
+
"""One external tool interested in Limbo's lifecycle.
|
|
24
|
+
|
|
25
|
+
Implementations must be fire-and-forget: never block, never raise.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def report(
|
|
29
|
+
self,
|
|
30
|
+
state: AgentState,
|
|
31
|
+
*,
|
|
32
|
+
message: str | None = None,
|
|
33
|
+
session_id: str | None = None,
|
|
34
|
+
session_path: str | None = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Report lifecycle state (optionally carrying session identity)."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
def report_session(
|
|
40
|
+
self, session_id: str, session_path: str | None = None
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Report session identity when it changes independently of state."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
def release(self) -> None:
|
|
46
|
+
"""Release any held authority/resources (on agent exit)."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CompositeReporter:
|
|
51
|
+
"""Fans lifecycle reports out to every active integration.
|
|
52
|
+
|
|
53
|
+
Always safe to call, even with zero reporters — call sites never need
|
|
54
|
+
to know which (if any) integrations are active.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, reporters: Iterable[IntegrationReporter] = ()): # noqa: D107
|
|
58
|
+
self._reporters = list(reporters)
|
|
59
|
+
|
|
60
|
+
def __bool__(self) -> bool:
|
|
61
|
+
return bool(self._reporters)
|
|
62
|
+
|
|
63
|
+
def report(
|
|
64
|
+
self,
|
|
65
|
+
state: AgentState,
|
|
66
|
+
*,
|
|
67
|
+
message: str | None = None,
|
|
68
|
+
session_id: str | None = None,
|
|
69
|
+
session_path: str | None = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
for reporter in self._reporters:
|
|
72
|
+
reporter.report(
|
|
73
|
+
state,
|
|
74
|
+
message=message,
|
|
75
|
+
session_id=session_id,
|
|
76
|
+
session_path=session_path,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def report_session(
|
|
80
|
+
self, session_id: str, session_path: str | None = None
|
|
81
|
+
) -> None:
|
|
82
|
+
for reporter in self._reporters:
|
|
83
|
+
reporter.report_session(session_id, session_path)
|
|
84
|
+
|
|
85
|
+
def release(self) -> None:
|
|
86
|
+
for reporter in self._reporters:
|
|
87
|
+
reporter.release()
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Herdr terminal multiplexer integration.
|
|
2
|
+
|
|
3
|
+
When Limbo runs inside a Herdr pane, it inherits ``HERDR_ENV``,
|
|
4
|
+
``HERDR_PANE_ID``, ``HERDR_BIN_PATH``, and ``HERDR_SOCKET_PATH``. This
|
|
5
|
+
reporter uses Herdr's CLI to report semantic lifecycle state
|
|
6
|
+
(``working``/``idle``/``blocked``) and native session identity, per
|
|
7
|
+
https://herdr.dev/docs/integrations/#integrate-your-own-agent
|
|
8
|
+
|
|
9
|
+
The integration is a strict no-op outside Herdr: ``from_env()`` returns
|
|
10
|
+
``None`` unless ``HERDR_ENV=1`` and both the pane id and a usable ``herdr``
|
|
11
|
+
binary (``HERDR_BIN_PATH``, else PATH) are present.
|
|
12
|
+
Reports are fire-and-forget CLI invocations — they must never block or
|
|
13
|
+
break the UI, and a missing/broken ``herdr`` binary is silently ignored.
|
|
14
|
+
|
|
15
|
+
``--source`` must stay stable and unique to this integration; ``--seq`` is
|
|
16
|
+
strictly increasing so Herdr can drop reports that arrive out of order
|
|
17
|
+
(turn-end and steer follow-up hand-offs race between workers).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import shutil
|
|
24
|
+
import subprocess
|
|
25
|
+
from collections.abc import Mapping
|
|
26
|
+
|
|
27
|
+
from limbo.integrations.base import AgentState
|
|
28
|
+
|
|
29
|
+
SOURCE = "custom:limbo"
|
|
30
|
+
AGENT = "limbo"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HerdrReporter:
|
|
34
|
+
"""Fire-and-forget lifecycle reporter bound to one Herdr pane."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, pane_id: str, bin_path: str):
|
|
37
|
+
self._pane_id = pane_id
|
|
38
|
+
self._bin_path = bin_path
|
|
39
|
+
self._seq = 0
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_env(cls, env: Mapping[str, str] | None = None) -> HerdrReporter | None:
|
|
43
|
+
"""Create a reporter when running inside a Herdr pane, else None.
|
|
44
|
+
|
|
45
|
+
``HERDR_BIN_PATH`` is preferred (portable across PATH differences)
|
|
46
|
+
but is not guaranteed to be set — fall back to a PATH lookup.
|
|
47
|
+
"""
|
|
48
|
+
env = os.environ if env is None else env
|
|
49
|
+
if env.get("HERDR_ENV") != "1":
|
|
50
|
+
return None
|
|
51
|
+
pane_id = env.get("HERDR_PANE_ID")
|
|
52
|
+
bin_path = env.get("HERDR_BIN_PATH") or shutil.which("herdr")
|
|
53
|
+
if not pane_id or not bin_path:
|
|
54
|
+
return None
|
|
55
|
+
return cls(pane_id, bin_path)
|
|
56
|
+
|
|
57
|
+
def report(
|
|
58
|
+
self,
|
|
59
|
+
state: AgentState,
|
|
60
|
+
*,
|
|
61
|
+
message: str | None = None,
|
|
62
|
+
session_id: str | None = None,
|
|
63
|
+
session_path: str | None = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Report lifecycle state (optionally with session identity)."""
|
|
66
|
+
cmd = [
|
|
67
|
+
self._bin_path,
|
|
68
|
+
"pane",
|
|
69
|
+
"report-agent",
|
|
70
|
+
self._pane_id,
|
|
71
|
+
"--source",
|
|
72
|
+
SOURCE,
|
|
73
|
+
"--agent",
|
|
74
|
+
AGENT,
|
|
75
|
+
"--state",
|
|
76
|
+
state,
|
|
77
|
+
"--seq",
|
|
78
|
+
str(self._next_seq()),
|
|
79
|
+
]
|
|
80
|
+
if message:
|
|
81
|
+
cmd += ["--message", message]
|
|
82
|
+
if session_id:
|
|
83
|
+
cmd += ["--agent-session-id", session_id]
|
|
84
|
+
if session_path:
|
|
85
|
+
cmd += ["--agent-session-path", session_path]
|
|
86
|
+
self._run(cmd)
|
|
87
|
+
|
|
88
|
+
def report_session(
|
|
89
|
+
self, session_id: str, session_path: str | None = None
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Report session identity when it changes independently of state
|
|
92
|
+
(``/new``, ``/sessions`` switch)."""
|
|
93
|
+
cmd = [
|
|
94
|
+
self._bin_path,
|
|
95
|
+
"pane",
|
|
96
|
+
"report-agent-session",
|
|
97
|
+
self._pane_id,
|
|
98
|
+
"--source",
|
|
99
|
+
SOURCE,
|
|
100
|
+
"--agent",
|
|
101
|
+
AGENT,
|
|
102
|
+
"--seq",
|
|
103
|
+
str(self._next_seq()),
|
|
104
|
+
"--agent-session-id",
|
|
105
|
+
session_id,
|
|
106
|
+
]
|
|
107
|
+
if session_path:
|
|
108
|
+
cmd += ["--agent-session-path", session_path]
|
|
109
|
+
self._run(cmd)
|
|
110
|
+
|
|
111
|
+
def release(self) -> None:
|
|
112
|
+
"""Release this source's lifecycle authority (on agent exit)."""
|
|
113
|
+
self._run(
|
|
114
|
+
[
|
|
115
|
+
self._bin_path,
|
|
116
|
+
"pane",
|
|
117
|
+
"release-agent",
|
|
118
|
+
self._pane_id,
|
|
119
|
+
"--source",
|
|
120
|
+
SOURCE,
|
|
121
|
+
"--agent",
|
|
122
|
+
AGENT,
|
|
123
|
+
]
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def _next_seq(self) -> int:
|
|
127
|
+
# Assigned synchronously before spawning, so sequence order matches
|
|
128
|
+
# call order even when deliveries race.
|
|
129
|
+
self._seq += 1
|
|
130
|
+
return self._seq
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def _run(cmd: list[str]) -> None:
|
|
134
|
+
try:
|
|
135
|
+
subprocess.Popen(
|
|
136
|
+
cmd,
|
|
137
|
+
stdin=subprocess.DEVNULL,
|
|
138
|
+
stdout=subprocess.DEVNULL,
|
|
139
|
+
stderr=subprocess.DEVNULL,
|
|
140
|
+
)
|
|
141
|
+
except OSError:
|
|
142
|
+
# A missing/broken herdr binary must never break the session.
|
|
143
|
+
pass
|
|
@@ -41,6 +41,7 @@ from limbo.goal_driver import (
|
|
|
41
41
|
GoalVerifyResultEvent,
|
|
42
42
|
GoalVerifyStarted,
|
|
43
43
|
)
|
|
44
|
+
from limbo.integrations import AgentState, create_reporters
|
|
44
45
|
from limbo.llm.client import LLMClient
|
|
45
46
|
from limbo.llm.factory import create_llm_client
|
|
46
47
|
from limbo.model_switch import (
|
|
@@ -111,6 +112,9 @@ class MainScreen(Screen[None]):
|
|
|
111
112
|
self._compact_busy = False
|
|
112
113
|
self._commands = SlashCommandRegistry()
|
|
113
114
|
self._register_builtin_commands()
|
|
115
|
+
# External tool integrations (Herdr, ...): an empty composite
|
|
116
|
+
# outside any integrated environment, so all calls are no-ops.
|
|
117
|
+
self._integrations = create_reporters()
|
|
114
118
|
|
|
115
119
|
@property
|
|
116
120
|
def _agent_busy(self) -> bool:
|
|
@@ -140,6 +144,20 @@ class MainScreen(Screen[None]):
|
|
|
140
144
|
resume=resume,
|
|
141
145
|
)
|
|
142
146
|
|
|
147
|
+
# -- External tool integrations (no-op outside integrated environments) --
|
|
148
|
+
|
|
149
|
+
def _report_state(
|
|
150
|
+
self, state: AgentState, *, message: str | None = None
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Report semantic lifecycle state to active integrations."""
|
|
153
|
+
self._integrations.report(state, message=message)
|
|
154
|
+
|
|
155
|
+
def _report_session(self) -> None:
|
|
156
|
+
"""Report the current session identity (startup, /new, /sessions)."""
|
|
157
|
+
self._integrations.report_session(
|
|
158
|
+
self.agent.session_id, str(self.agent.session_path)
|
|
159
|
+
)
|
|
160
|
+
|
|
143
161
|
def compose(self) -> ComposeResult:
|
|
144
162
|
yield StatusBar(
|
|
145
163
|
model=self.config.llm.model,
|
|
@@ -169,6 +187,8 @@ class MainScreen(Screen[None]):
|
|
|
169
187
|
)
|
|
170
188
|
self._refresh_goal_indicator() # D6: silent resume, badge only
|
|
171
189
|
self.query_one("#input", InputWidget).focus()
|
|
190
|
+
self._report_session()
|
|
191
|
+
self._report_state("idle")
|
|
172
192
|
|
|
173
193
|
# -- slash command menu ---------------------------------------------------
|
|
174
194
|
|
|
@@ -332,6 +352,7 @@ class MainScreen(Screen[None]):
|
|
|
332
352
|
self._pending_proposals = parsed or []
|
|
333
353
|
if not self._pending_proposals:
|
|
334
354
|
chat.add_info("未能解析模型的验收提议,请手动输入或跳过")
|
|
355
|
+
self._report_state("blocked", message="等待确认 /goal 验收命令")
|
|
335
356
|
self.app.push_screen(
|
|
336
357
|
VerifyPicker(self._pending_proposals), self._on_verify_picked
|
|
337
358
|
)
|
|
@@ -345,6 +366,7 @@ class MainScreen(Screen[None]):
|
|
|
345
366
|
if choice is None:
|
|
346
367
|
chat.add_info("已跳过自动验收(单轮模式);/goal clear 可退出 goal")
|
|
347
368
|
self._refresh_goal_indicator()
|
|
369
|
+
self._report_state("idle")
|
|
348
370
|
return
|
|
349
371
|
if choice == VERIFY_EDIT:
|
|
350
372
|
# Prefill the best proposal for editing; the next submission is
|
|
@@ -356,6 +378,7 @@ class MainScreen(Screen[None]):
|
|
|
356
378
|
input_widget.focus()
|
|
357
379
|
self._editing_verify = True
|
|
358
380
|
chat.add_info("编辑验收命令后回车确认(清空回车则跳过)")
|
|
381
|
+
self._report_state("blocked", message="等待编辑 /goal 验收命令")
|
|
359
382
|
return
|
|
360
383
|
self._accept_verify(choice)
|
|
361
384
|
|
|
@@ -364,6 +387,7 @@ class MainScreen(Screen[None]):
|
|
|
364
387
|
if is_dangerous(command, self.config.safety.dangerous_commands):
|
|
365
388
|
chat.add_error(f"验收命令命中危险命令过滤,已拒绝:{command}")
|
|
366
389
|
chat.add_info("goal 保持单轮模式;/goal clear 退出")
|
|
390
|
+
self._report_state("idle")
|
|
367
391
|
return
|
|
368
392
|
state = self.driver.set_verify(command)
|
|
369
393
|
if state is None:
|
|
@@ -484,6 +508,7 @@ class MainScreen(Screen[None]):
|
|
|
484
508
|
input_widget.disabled = True
|
|
485
509
|
self._compact_busy = True
|
|
486
510
|
statusbar.set_state("compacting…", "thinking")
|
|
511
|
+
self._report_state("working")
|
|
487
512
|
try:
|
|
488
513
|
async for event in self.agent.compact(trigger="manual"):
|
|
489
514
|
await self._process_agent_event(event)
|
|
@@ -492,6 +517,7 @@ class MainScreen(Screen[None]):
|
|
|
492
517
|
input_widget.disabled = False
|
|
493
518
|
input_widget.focus()
|
|
494
519
|
statusbar.set_state("idle")
|
|
520
|
+
self._report_state("idle")
|
|
495
521
|
|
|
496
522
|
def _show_help(self) -> None:
|
|
497
523
|
self.query_one("#chat", ChatWidget).add_info(self._commands.help_text())
|
|
@@ -549,6 +575,7 @@ class MainScreen(Screen[None]):
|
|
|
549
575
|
self.agent = self._new_agent()
|
|
550
576
|
chat.clear()
|
|
551
577
|
chat.add_info(f"已开始新会话 {self.agent.session_id}")
|
|
578
|
+
self._report_session()
|
|
552
579
|
|
|
553
580
|
def _find_skill(self, name: str) -> Skill | None:
|
|
554
581
|
if self._commands.get(f"/{name}") is not None:
|
|
@@ -602,6 +629,7 @@ class MainScreen(Screen[None]):
|
|
|
602
629
|
self._render_history()
|
|
603
630
|
meta = self.agent.session_meta
|
|
604
631
|
chat.add_info(f"已切换到会话 {meta.id} · {meta.title or '(无标题)'}")
|
|
632
|
+
self._report_session()
|
|
605
633
|
|
|
606
634
|
def _render_history(self) -> None:
|
|
607
635
|
"""Render the agent's restored history into the chat flow.
|
|
@@ -684,6 +712,7 @@ class MainScreen(Screen[None]):
|
|
|
684
712
|
async def on_unmount(self) -> None:
|
|
685
713
|
"""Close owned resources on shutdown: the LLM client's HTTP pool and
|
|
686
714
|
the agent's trace file handle."""
|
|
715
|
+
self._integrations.release()
|
|
687
716
|
self.agent.close()
|
|
688
717
|
close = getattr(self.llm_client, "close", None)
|
|
689
718
|
if close is not None:
|
|
@@ -707,6 +736,7 @@ class MainScreen(Screen[None]):
|
|
|
707
736
|
self._editing_verify = False
|
|
708
737
|
chat = self.query_one("#chat", ChatWidget)
|
|
709
738
|
if text.startswith("/"):
|
|
739
|
+
self._report_state("idle")
|
|
710
740
|
self._handle_command(text)
|
|
711
741
|
return
|
|
712
742
|
command = text.strip()
|
|
@@ -715,6 +745,7 @@ class MainScreen(Screen[None]):
|
|
|
715
745
|
self._accept_verify(command)
|
|
716
746
|
else:
|
|
717
747
|
chat.add_info("已跳过自动验收(单轮模式);/goal clear 可退出 goal")
|
|
748
|
+
self._report_state("idle")
|
|
718
749
|
return
|
|
719
750
|
if text.startswith("/"):
|
|
720
751
|
self._handle_command(text)
|
|
@@ -825,6 +856,7 @@ class MainScreen(Screen[None]):
|
|
|
825
856
|
# go to the steer queue instead of being blocked. Single-flight is
|
|
826
857
|
# owned by the goal driver (``running`` is set eagerly on call).
|
|
827
858
|
statusbar.set_state("thinking…", "thinking")
|
|
859
|
+
self._report_state("working")
|
|
828
860
|
# driver.run() sets ``running`` eagerly at call time, so the badge
|
|
829
861
|
# refresh below already sees the loop as executing (rainbow on).
|
|
830
862
|
events = self.driver.run(user_input, attachments)
|
|
@@ -858,6 +890,7 @@ class MainScreen(Screen[None]):
|
|
|
858
890
|
# the user can keep typing without clicking.
|
|
859
891
|
input_widget.focus()
|
|
860
892
|
statusbar.set_state("idle")
|
|
893
|
+
self._report_state("idle")
|
|
861
894
|
self._refresh_goal_indicator()
|
|
862
895
|
# M2: a proposal round that ends without leftover steers pops
|
|
863
896
|
# the verify-command confirmation picker.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Tests for the Herdr integration reporter (limbo.integrations.herdr)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
from limbo.integrations.herdr import AGENT, SOURCE, HerdrReporter
|
|
9
|
+
|
|
10
|
+
FULL_ENV = {
|
|
11
|
+
"HERDR_ENV": "1",
|
|
12
|
+
"HERDR_PANE_ID": "w1:p2",
|
|
13
|
+
"HERDR_BIN_PATH": "/usr/local/bin/herdr",
|
|
14
|
+
"HERDR_SOCKET_PATH": "/tmp/herdr.sock",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def make_reporter(monkeypatch) -> tuple[HerdrReporter, list[list[str]]]:
|
|
19
|
+
"""A reporter with Popen captured; returns (reporter, recorded argv)."""
|
|
20
|
+
calls: list[list[str]] = []
|
|
21
|
+
|
|
22
|
+
def fake_popen(cmd, **kwargs):
|
|
23
|
+
calls.append(list(cmd))
|
|
24
|
+
|
|
25
|
+
class _Proc:
|
|
26
|
+
def wait(self, timeout=None):
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
return _Proc()
|
|
30
|
+
|
|
31
|
+
monkeypatch.setattr(subprocess, "Popen", fake_popen)
|
|
32
|
+
reporter = HerdrReporter.from_env(FULL_ENV)
|
|
33
|
+
assert reporter is not None
|
|
34
|
+
return reporter, calls
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_from_env_outside_herdr_is_none():
|
|
38
|
+
assert HerdrReporter.from_env({}) is None
|
|
39
|
+
assert HerdrReporter.from_env({"HERDR_ENV": "0"}) is None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_from_env_requires_pane_and_bin(monkeypatch):
|
|
43
|
+
# No herdr binary anywhere: even a full-looking env cannot activate.
|
|
44
|
+
monkeypatch.setattr(shutil, "which", lambda name: None)
|
|
45
|
+
assert HerdrReporter.from_env({"HERDR_ENV": "1"}) is None
|
|
46
|
+
assert (
|
|
47
|
+
HerdrReporter.from_env({"HERDR_ENV": "1", "HERDR_PANE_ID": "w1:p1"}) is None
|
|
48
|
+
)
|
|
49
|
+
assert (
|
|
50
|
+
HerdrReporter.from_env(
|
|
51
|
+
{"HERDR_ENV": "1", "HERDR_BIN_PATH": "/bin/herdr"}
|
|
52
|
+
)
|
|
53
|
+
is None
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_from_env_falls_back_to_path_lookup(monkeypatch):
|
|
58
|
+
# HERDR_BIN_PATH is not guaranteed to be set; find herdr on PATH.
|
|
59
|
+
monkeypatch.setattr(shutil, "which", lambda name: "/resolved/herdr")
|
|
60
|
+
env = {"HERDR_ENV": "1", "HERDR_PANE_ID": "w1:p1", "HERDR_BIN_PATH": ""}
|
|
61
|
+
reporter = HerdrReporter.from_env(env)
|
|
62
|
+
assert isinstance(reporter, HerdrReporter)
|
|
63
|
+
assert reporter._bin_path == "/resolved/herdr"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_from_env_inside_herdr():
|
|
67
|
+
reporter = HerdrReporter.from_env(FULL_ENV)
|
|
68
|
+
assert isinstance(reporter, HerdrReporter)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_report_state_command_and_seq(monkeypatch):
|
|
72
|
+
reporter, calls = make_reporter(monkeypatch)
|
|
73
|
+
reporter.report("working")
|
|
74
|
+
reporter.report("idle")
|
|
75
|
+
|
|
76
|
+
assert calls[0] == [
|
|
77
|
+
"/usr/local/bin/herdr",
|
|
78
|
+
"pane",
|
|
79
|
+
"report-agent",
|
|
80
|
+
"w1:p2",
|
|
81
|
+
"--source",
|
|
82
|
+
SOURCE,
|
|
83
|
+
"--agent",
|
|
84
|
+
AGENT,
|
|
85
|
+
"--state",
|
|
86
|
+
"working",
|
|
87
|
+
"--seq",
|
|
88
|
+
"1",
|
|
89
|
+
]
|
|
90
|
+
# Strictly increasing seq across reports from the same source.
|
|
91
|
+
assert calls[1][calls[1].index("--seq") + 1] == "2"
|
|
92
|
+
assert calls[1][calls[1].index("--state") + 1] == "idle"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_report_with_message_and_session(monkeypatch):
|
|
96
|
+
reporter, calls = make_reporter(monkeypatch)
|
|
97
|
+
reporter.report(
|
|
98
|
+
"blocked",
|
|
99
|
+
message="等待确认",
|
|
100
|
+
session_id="sess-1",
|
|
101
|
+
session_path="/tmp/sess-1.jsonl",
|
|
102
|
+
)
|
|
103
|
+
cmd = calls[0]
|
|
104
|
+
assert cmd[cmd.index("--message") + 1] == "等待确认"
|
|
105
|
+
assert cmd[cmd.index("--agent-session-id") + 1] == "sess-1"
|
|
106
|
+
assert cmd[cmd.index("--agent-session-path") + 1] == "/tmp/sess-1.jsonl"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_report_session_command(monkeypatch):
|
|
110
|
+
reporter, calls = make_reporter(monkeypatch)
|
|
111
|
+
reporter.report("idle")
|
|
112
|
+
reporter.report_session("sess-2", "/tmp/sess-2.jsonl")
|
|
113
|
+
cmd = calls[1]
|
|
114
|
+
assert cmd[:3] == ["/usr/local/bin/herdr", "pane", "report-agent-session"]
|
|
115
|
+
assert cmd[cmd.index("--agent-session-id") + 1] == "sess-2"
|
|
116
|
+
assert cmd[cmd.index("--agent-session-path") + 1] == "/tmp/sess-2.jsonl"
|
|
117
|
+
# Session reports share the same increasing sequence as state reports.
|
|
118
|
+
assert cmd[cmd.index("--seq") + 1] == "2"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_release_command(monkeypatch):
|
|
122
|
+
reporter, calls = make_reporter(monkeypatch)
|
|
123
|
+
reporter.release()
|
|
124
|
+
assert calls[0] == [
|
|
125
|
+
"/usr/local/bin/herdr",
|
|
126
|
+
"pane",
|
|
127
|
+
"release-agent",
|
|
128
|
+
"w1:p2",
|
|
129
|
+
"--source",
|
|
130
|
+
SOURCE,
|
|
131
|
+
"--agent",
|
|
132
|
+
AGENT,
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_popen_failure_is_swallowed(monkeypatch):
|
|
137
|
+
def boom(cmd, **kwargs):
|
|
138
|
+
raise OSError("no such file")
|
|
139
|
+
|
|
140
|
+
monkeypatch.setattr(subprocess, "Popen", boom)
|
|
141
|
+
reporter = HerdrReporter.from_env(FULL_ENV)
|
|
142
|
+
assert reporter is not None
|
|
143
|
+
reporter.report("working") # must not raise
|
|
144
|
+
reporter.report_session("s") # must not raise
|
|
145
|
+
reporter.release() # must not raise
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Tests for the integration abstraction layer (limbo.integrations)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from limbo.integrations import CompositeReporter, create_reporters
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FakeReporter:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.calls: list[tuple] = []
|
|
11
|
+
|
|
12
|
+
def report(self, state, *, message=None, session_id=None, session_path=None):
|
|
13
|
+
self.calls.append(("report", state, message, session_id, session_path))
|
|
14
|
+
|
|
15
|
+
def report_session(self, session_id, session_path=None):
|
|
16
|
+
self.calls.append(("report_session", session_id, session_path))
|
|
17
|
+
|
|
18
|
+
def release(self):
|
|
19
|
+
self.calls.append(("release",))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_empty_composite_is_a_safe_noop():
|
|
23
|
+
composite = CompositeReporter()
|
|
24
|
+
assert not composite
|
|
25
|
+
composite.report("working") # must not raise
|
|
26
|
+
composite.report_session("s", "/tmp/s.jsonl")
|
|
27
|
+
composite.release()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_composite_fans_out_to_all_reporters():
|
|
31
|
+
a, b = FakeReporter(), FakeReporter()
|
|
32
|
+
composite = CompositeReporter([a, b])
|
|
33
|
+
assert composite
|
|
34
|
+
|
|
35
|
+
composite.report("blocked", message="等待确认")
|
|
36
|
+
composite.report_session("sess-1", "/tmp/sess-1.jsonl")
|
|
37
|
+
composite.release()
|
|
38
|
+
|
|
39
|
+
expected = [
|
|
40
|
+
("report", "blocked", "等待确认", None, None),
|
|
41
|
+
("report_session", "sess-1", "/tmp/sess-1.jsonl"),
|
|
42
|
+
("release",),
|
|
43
|
+
]
|
|
44
|
+
assert a.calls == expected
|
|
45
|
+
assert b.calls == expected
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_create_reporters_outside_any_tool_is_empty():
|
|
49
|
+
assert not create_reporters({})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_create_reporters_detects_herdr():
|
|
53
|
+
composite = create_reporters(
|
|
54
|
+
{
|
|
55
|
+
"HERDR_ENV": "1",
|
|
56
|
+
"HERDR_PANE_ID": "w1:p1",
|
|
57
|
+
"HERDR_BIN_PATH": "/bin/herdr",
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
assert composite
|
|
File without changes
|
|
File without changes
|
{limbo_code-0.1.13 → limbo_code-0.1.14}/.agents/skills/improve-codebase-architecture/HTML-REPORT.md
RENAMED
|
File without changes
|
{limbo_code-0.1.13 → limbo_code-0.1.14}/.agents/skills/improve-codebase-architecture/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|