novacode-cli 1.2.0__py3-none-any.whl
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.
- novacode_cli/__init__.py +29 -0
- novacode_cli/__main__.py +6 -0
- novacode_cli/_lazy_heavy.py +99 -0
- novacode_cli/_version.py +3 -0
- novacode_cli/agent_stream.py +178 -0
- novacode_cli/agents/agent_file.py +250 -0
- novacode_cli/agents/agent_mailbox.py +120 -0
- novacode_cli/agents/approval_decisions.py +146 -0
- novacode_cli/agents/arg_repair.py +196 -0
- novacode_cli/agents/async_agents/__init__.py +6 -0
- novacode_cli/agents/async_agents/_config.py +213 -0
- novacode_cli/agents/async_agents/_mcp_tools.py +192 -0
- novacode_cli/agents/async_agents/_specialist.py +84 -0
- novacode_cli/agents/async_agents/_test_commands.py +89 -0
- novacode_cli/agents/async_agents/_tools.py +432 -0
- novacode_cli/agents/async_agents/general_purpose.py +42 -0
- novacode_cli/agents/async_agents/langgraph.json +9 -0
- novacode_cli/agents/async_agents/research.py +22 -0
- novacode_cli/agents/async_context.py +303 -0
- novacode_cli/agents/async_on_demand.py +52 -0
- novacode_cli/agents/async_task_errors.py +75 -0
- novacode_cli/agents/async_workspace.py +27 -0
- novacode_cli/agents/attribution.py +165 -0
- novacode_cli/agents/browser_mode.py +119 -0
- novacode_cli/agents/btw_agent.py +71 -0
- novacode_cli/agents/caveman.py +352 -0
- novacode_cli/agents/caveman_support.py +370 -0
- novacode_cli/agents/code_mode.py +774 -0
- novacode_cli/agents/code_runtime.py +160 -0
- novacode_cli/agents/context_compaction.py +1339 -0
- novacode_cli/agents/core_agent.py +2710 -0
- novacode_cli/agents/default_subagents/__init__.py +24 -0
- novacode_cli/agents/default_subagents/async_subagents.py +296 -0
- novacode_cli/agents/default_subagents/builtin/browser-automation-agent.md +110 -0
- novacode_cli/agents/default_subagents/builtin/bug-fix-agent.md +22 -0
- novacode_cli/agents/default_subagents/builtin/bug-investigation-agent.md +42 -0
- novacode_cli/agents/default_subagents/builtin/code-explorer.md +37 -0
- novacode_cli/agents/default_subagents/builtin/code-review-agent.md +33 -0
- novacode_cli/agents/default_subagents/builtin/dependency-audit-agent.md +34 -0
- novacode_cli/agents/default_subagents/builtin/documentation-update-agent.md +46 -0
- novacode_cli/agents/default_subagents/builtin/performance-audit-agent.md +41 -0
- novacode_cli/agents/default_subagents/builtin/plan-scout-agent.md +64 -0
- novacode_cli/agents/default_subagents/builtin/refactoring-agent.md +35 -0
- novacode_cli/agents/default_subagents/builtin/refactoring-specialist-agent.md +175 -0
- novacode_cli/agents/default_subagents/builtin/security-audit-agent.md +96 -0
- novacode_cli/agents/default_subagents/builtin/test-generation-agent.md +33 -0
- novacode_cli/agents/default_subagents/builtin/test-runner-agent.md +55 -0
- novacode_cli/agents/default_subagents/prompt.py +88 -0
- novacode_cli/agents/default_subagents/subagents.py +138 -0
- novacode_cli/agents/dream_agent.py +93 -0
- novacode_cli/agents/edit_diagnostics.py +362 -0
- novacode_cli/agents/edit_fallback.py +62 -0
- novacode_cli/agents/handoff.py +166 -0
- novacode_cli/agents/invocation.py +152 -0
- novacode_cli/agents/model_budget.py +42 -0
- novacode_cli/agents/model_retry.py +113 -0
- novacode_cli/agents/model_router.py +420 -0
- novacode_cli/agents/openai_decisions.py +186 -0
- novacode_cli/agents/plan_agent/__init__.py +12 -0
- novacode_cli/agents/plan_agent/plan_agent.py +357 -0
- novacode_cli/agents/plan_agent/plan_mode_middleware.py +64 -0
- novacode_cli/agents/plan_execution.py +237 -0
- novacode_cli/agents/ponytail.py +66 -0
- novacode_cli/agents/prime.py +274 -0
- novacode_cli/agents/provider_caching.py +99 -0
- novacode_cli/agents/server_launcher.py +904 -0
- novacode_cli/agents/strict_tools.py +384 -0
- novacode_cli/agents/task_discipline.py +294 -0
- novacode_cli/agents/tool_call_repair.py +290 -0
- novacode_cli/agents/tool_offload.py +291 -0
- novacode_cli/agents/tool_search.py +806 -0
- novacode_cli/agents/tool_search_variants.py +106 -0
- novacode_cli/agents/tool_verdicts.py +1194 -0
- novacode_cli/agents/user_async_agents.py +312 -0
- novacode_cli/artifacts/__init__.py +10 -0
- novacode_cli/artifacts/registry.py +278 -0
- novacode_cli/artifacts/server.py +426 -0
- novacode_cli/assets/Nova.png +0 -0
- novacode_cli/assets/nova-avatar.png +0 -0
- novacode_cli/audio/__init__.py +301 -0
- novacode_cli/audio/capture.py +137 -0
- novacode_cli/audio/downloads.py +273 -0
- novacode_cli/audio/pipeline.py +391 -0
- novacode_cli/audio/playback.py +87 -0
- novacode_cli/audio/providers.py +208 -0
- novacode_cli/audio/speakable.py +78 -0
- novacode_cli/audio/stt.py +132 -0
- novacode_cli/audio/stt_deepgram.py +69 -0
- novacode_cli/audio/stt_elevenlabs.py +89 -0
- novacode_cli/audio/stt_parakeet.py +94 -0
- novacode_cli/audio/stt_whistle.py +61 -0
- novacode_cli/audio/summarize.py +92 -0
- novacode_cli/audio/tts.py +85 -0
- novacode_cli/audio/tts_kokoro.py +73 -0
- novacode_cli/audio/tts_orpheus.py +180 -0
- novacode_cli/audio/tts_pocket.py +131 -0
- novacode_cli/audio/tts_sano.py +69 -0
- novacode_cli/audio/vad.py +145 -0
- novacode_cli/backends/__init__.py +17 -0
- novacode_cli/backends/filesystem.py +894 -0
- novacode_cli/backends/native_search.py +92 -0
- novacode_cli/bootstrap/__init__.py +9 -0
- novacode_cli/bootstrap/env_snapshot.py +240 -0
- novacode_cli/bootstrap/steering.py +464 -0
- novacode_cli/bootstrap/vision_router.py +604 -0
- novacode_cli/brand.py +362 -0
- novacode_cli/cli_args.py +718 -0
- novacode_cli/commands/__init__.py +237 -0
- novacode_cli/commands/agents_commands.py +231 -0
- novacode_cli/commands/auth_handler.py +40 -0
- novacode_cli/commands/browser_use_handler.py +75 -0
- novacode_cli/commands/cache_handler.py +32 -0
- novacode_cli/commands/chat_handler.py +1357 -0
- novacode_cli/commands/ci_fix.py +171 -0
- novacode_cli/commands/claude_plan.py +331 -0
- novacode_cli/commands/codex_plan.py +195 -0
- novacode_cli/commands/commands.py +1113 -0
- novacode_cli/commands/create_handler.py +124 -0
- novacode_cli/commands/create_server.py +1887 -0
- novacode_cli/commands/cron_handler.py +119 -0
- novacode_cli/commands/dream_handler.py +203 -0
- novacode_cli/commands/effort_handler.py +65 -0
- novacode_cli/commands/evolution_handler.py +214 -0
- novacode_cli/commands/file_commands.py +381 -0
- novacode_cli/commands/handoff_handler.py +68 -0
- novacode_cli/commands/hooks_handler.py +269 -0
- novacode_cli/commands/init_handler.py +146 -0
- novacode_cli/commands/learning_handler.py +160 -0
- novacode_cli/commands/lite_mode_handler.py +76 -0
- novacode_cli/commands/log_commands.py +363 -0
- novacode_cli/commands/mcp_handler.py +29 -0
- novacode_cli/commands/model_handler.py +29 -0
- novacode_cli/commands/notifications_handler.py +158 -0
- novacode_cli/commands/onboard_handler.py +121 -0
- novacode_cli/commands/plan_handler.py +440 -0
- novacode_cli/commands/planner_usage.py +250 -0
- novacode_cli/commands/plugin_install_handler.py +38 -0
- novacode_cli/commands/plugins_handler.py +35 -0
- novacode_cli/commands/prime_handler.py +455 -0
- novacode_cli/commands/prompt_handler.py +149 -0
- novacode_cli/commands/provider_usage.py +160 -0
- novacode_cli/commands/ralph_events.py +106 -0
- novacode_cli/commands/ralph_handler.py +1293 -0
- novacode_cli/commands/refine_handler.py +213 -0
- novacode_cli/commands/research_handler.py +202 -0
- novacode_cli/commands/rootapp_remote.py +83 -0
- novacode_cli/commands/router_handler.py +36 -0
- novacode_cli/commands/server_commands.py +249 -0
- novacode_cli/commands/session_commands.py +203 -0
- novacode_cli/commands/share_handler.py +450 -0
- novacode_cli/commands/side_commands.py +366 -0
- novacode_cli/commands/skill_invoke.py +420 -0
- novacode_cli/commands/skills_commands.py +48 -0
- novacode_cli/commands/trace_handler.py +322 -0
- novacode_cli/commands/trello_handler.py +226 -0
- novacode_cli/commands/trello_remote.py +177 -0
- novacode_cli/commands/trello_server.py +1214 -0
- novacode_cli/commands/vision_handler.py +127 -0
- novacode_cli/commands/voice_handler.py +447 -0
- novacode_cli/commands/webhook_handler.py +134 -0
- novacode_cli/commands/wiki_commands.py +227 -0
- novacode_cli/compaction.py +753 -0
- novacode_cli/computation_cache.py +159 -0
- novacode_cli/computer/__init__.py +1 -0
- novacode_cli/computer/persistent.py +234 -0
- novacode_cli/computer/preview.py +253 -0
- novacode_cli/computer/runtime.py +474 -0
- novacode_cli/computer/windows_console.py +152 -0
- novacode_cli/computer/windows_qemu.py +211 -0
- novacode_cli/config/browser_mode.py +159 -0
- novacode_cli/config/catalog_models.py +95 -0
- novacode_cli/config/config.py +1388 -0
- novacode_cli/config/credentials.py +540 -0
- novacode_cli/config/google_oauth_auth.py +281 -0
- novacode_cli/config/model_capabilities.py +343 -0
- novacode_cli/config/model_catalog.py +275 -0
- novacode_cli/config/model_create.py +1325 -0
- novacode_cli/config/model_manager.py +695 -0
- novacode_cli/config/nova_config.py +1283 -0
- novacode_cli/config/openai_chatgpt_auth.py +384 -0
- novacode_cli/config/opencode_gateway.py +25 -0
- novacode_cli/config/plan_mode.py +188 -0
- novacode_cli/config/provider_auth.py +438 -0
- novacode_cli/config/provider_catalog.json +5127 -0
- novacode_cli/config/provider_registry.py +38 -0
- novacode_cli/config/provider_transports.py +258 -0
- novacode_cli/config/role_models.py +175 -0
- novacode_cli/context/__init__.py +50 -0
- novacode_cli/context/_analysis.py +799 -0
- novacode_cli/context/_budget.py +116 -0
- novacode_cli/context/_conversation.py +73 -0
- novacode_cli/context/_dynamic.py +264 -0
- novacode_cli/context/_model_config.py +380 -0
- novacode_cli/context/_models_dev.py +99 -0
- novacode_cli/context/archives.py +248 -0
- novacode_cli/context/estimates.py +65 -0
- novacode_cli/context/history.py +17 -0
- novacode_cli/context/knowledge_cache.py +292 -0
- novacode_cli/context/manager.py +101 -0
- novacode_cli/context/observations.py +65 -0
- novacode_cli/context/pressure.py +168 -0
- novacode_cli/context/provider_accounting.py +109 -0
- novacode_cli/context/snapshot.py +493 -0
- novacode_cli/context/strategies.py +279 -0
- novacode_cli/context/task_progress.py +122 -0
- novacode_cli/core/__init__.py +0 -0
- novacode_cli/core/agent_loop.py +1821 -0
- novacode_cli/core/autonomous_loop.py +277 -0
- novacode_cli/core/code_approvals.py +57 -0
- novacode_cli/core/input_preparation.py +276 -0
- novacode_cli/core/reasoning.py +67 -0
- novacode_cli/core/streaming.py +326 -0
- novacode_cli/core/subagent_tasks.py +696 -0
- novacode_cli/core/subagent_tracking.py +235 -0
- novacode_cli/core/tool_results.py +116 -0
- novacode_cli/core/verification_loop.py +272 -0
- novacode_cli/council.py +595 -0
- novacode_cli/council_consensus.py +115 -0
- novacode_cli/council_decision.py +157 -0
- novacode_cli/council_planner.py +1421 -0
- novacode_cli/council_settings.py +208 -0
- novacode_cli/council_web_settings.py +41 -0
- novacode_cli/council_workspace.py +56 -0
- novacode_cli/cowork/__init__.py +10 -0
- novacode_cli/cowork/broker_middleware.py +162 -0
- novacode_cli/cowork/launcher.py +102 -0
- novacode_cli/cowork/policy.py +284 -0
- novacode_cli/cowork/ui.py +826 -0
- novacode_cli/daemons/__init__.py +1 -0
- novacode_cli/daemons/registry.py +342 -0
- novacode_cli/desktop_notifications.py +302 -0
- novacode_cli/doctor.py +251 -0
- novacode_cli/embeddings.py +109 -0
- novacode_cli/entrypoint.py +56 -0
- novacode_cli/errors/__init__.py +19 -0
- novacode_cli/errors/handlers.py +648 -0
- novacode_cli/errors/provider_errors.py +249 -0
- novacode_cli/errors/taxonomy.py +80 -0
- novacode_cli/events.py +179 -0
- novacode_cli/file_ops.py +714 -0
- novacode_cli/git_safety.py +281 -0
- novacode_cli/handoff.py +383 -0
- novacode_cli/headless/__init__.py +18 -0
- novacode_cli/headless/input.py +58 -0
- novacode_cli/headless/output.py +224 -0
- novacode_cli/headless/pipe.py +532 -0
- novacode_cli/headless/runner.py +461 -0
- novacode_cli/hermes/__init__.py +39 -0
- novacode_cli/hermes/activation.py +297 -0
- novacode_cli/hermes/config.py +129 -0
- novacode_cli/hermes/curator.py +199 -0
- novacode_cli/hermes/evolution.py +374 -0
- novacode_cli/hermes/ledger.py +364 -0
- novacode_cli/hermes/memory_tiers.py +969 -0
- novacode_cli/hermes/middleware.py +678 -0
- novacode_cli/hermes/overview.py +174 -0
- novacode_cli/hermes/prompt_evolution.py +503 -0
- novacode_cli/hermes/refine_loop.py +643 -0
- novacode_cli/hermes/refinement_log.py +190 -0
- novacode_cli/hermes/review.py +674 -0
- novacode_cli/hermes/skill_debate.py +135 -0
- novacode_cli/hermes/skill_discovery.py +450 -0
- novacode_cli/hermes/skill_manager.py +335 -0
- novacode_cli/hermes/skill_proposals.py +43 -0
- novacode_cli/hermes/test_gaming.py +119 -0
- novacode_cli/hermes/tracker.py +435 -0
- novacode_cli/hermes/tuner.py +206 -0
- novacode_cli/hermes/verification_memory.py +134 -0
- novacode_cli/hermes/verifier.py +277 -0
- novacode_cli/hitl/__init__.py +22 -0
- novacode_cli/hitl/interrupts.py +493 -0
- novacode_cli/hooks.py +359 -0
- novacode_cli/image_utils.py +517 -0
- novacode_cli/init/__init__.py +7 -0
- novacode_cli/init/events.py +43 -0
- novacode_cli/input_utils.py +526 -0
- novacode_cli/install_commands.py +87 -0
- novacode_cli/integrations/__init__.py +82 -0
- novacode_cli/integrations/daytona.py +122 -0
- novacode_cli/integrations/docker.py +309 -0
- novacode_cli/integrations/host_path.py +111 -0
- novacode_cli/integrations/langsmith.py +151 -0
- novacode_cli/integrations/modal.py +134 -0
- novacode_cli/integrations/runloop.py +130 -0
- novacode_cli/integrations/sandbox_factory.py +1388 -0
- novacode_cli/integrations/sandbox_registry.py +312 -0
- novacode_cli/integrations/workdir_backend.py +589 -0
- novacode_cli/main.py +2078 -0
- novacode_cli/mcp/__init__.py +73 -0
- novacode_cli/mcp/auth.py +189 -0
- novacode_cli/mcp/client.py +571 -0
- novacode_cli/mcp/code_mode.py +5 -0
- novacode_cli/mcp/code_runtime.py +5 -0
- novacode_cli/mcp/commands.py +902 -0
- novacode_cli/mcp/config.py +406 -0
- novacode_cli/mcp/generic.py +128 -0
- novacode_cli/mcp/middleware.py +993 -0
- novacode_cli/mcp/presets.py +367 -0
- novacode_cli/memory/__init__.py +1 -0
- novacode_cli/memory/agent_memory.py +1312 -0
- novacode_cli/memory/builtin_lessons.py +60 -0
- novacode_cli/memory/dream_gates.py +192 -0
- novacode_cli/memory/limits.py +59 -0
- novacode_cli/memory/parsed_cache.py +106 -0
- novacode_cli/memory/recall_stats.py +97 -0
- novacode_cli/memory/semantic.py +116 -0
- novacode_cli/memory/store.py +335 -0
- novacode_cli/memory/wiki.py +274 -0
- novacode_cli/migrate.py +185 -0
- novacode_cli/onboarding.py +649 -0
- novacode_cli/onboarding_check.py +119 -0
- novacode_cli/path_approval.py +297 -0
- novacode_cli/plan_archive.py +148 -0
- novacode_cli/plugins/__init__.py +121 -0
- novacode_cli/plugins/claude_plugins.py +538 -0
- novacode_cli/plugins/cli.py +168 -0
- novacode_cli/plugins/components.py +413 -0
- novacode_cli/plugins/hooks.py +356 -0
- novacode_cli/plugins/loader.py +317 -0
- novacode_cli/plugins/marketplaces.py +199 -0
- novacode_cli/plugins/storage.py +70 -0
- novacode_cli/prime.py +759 -0
- novacode_cli/prime_factory/__init__.py +15 -0
- novacode_cli/prime_factory/container.py +198 -0
- novacode_cli/prime_factory/controller.py +565 -0
- novacode_cli/prime_factory/knowledge.py +261 -0
- novacode_cli/prime_factory/publication.py +218 -0
- novacode_cli/prime_factory/recovery.py +50 -0
- novacode_cli/prime_factory/research.py +370 -0
- novacode_cli/prime_factory/runner.py +608 -0
- novacode_cli/prime_factory/runtime.py +261 -0
- novacode_cli/prime_factory/state.py +201 -0
- novacode_cli/prime_factory/store.py +128 -0
- novacode_cli/prime_factory/worker.py +129 -0
- novacode_cli/prime_factory/workflow.py +899 -0
- novacode_cli/prime_factory/workspace.py +317 -0
- novacode_cli/prime_memory.py +290 -0
- novacode_cli/process_manager.py +585 -0
- novacode_cli/project_onboard.py +537 -0
- novacode_cli/prompts/System_Prompt_Nova.jinja +23 -0
- novacode_cli/prompts/__init__.py +168 -0
- novacode_cli/prompts/agent_definitions.jinja +11 -0
- novacode_cli/prompts/agent_generation.jinja +125 -0
- novacode_cli/prompts/async_agents/general_purpose.jinja +22 -0
- novacode_cli/prompts/claude_plan_mode.jinja +12 -0
- novacode_cli/prompts/coding_workflow.jinja +18 -0
- novacode_cli/prompts/computer_use.jinja +17 -0
- novacode_cli/prompts/core_agent_system.jinja +81 -0
- novacode_cli/prompts/delegation.jinja +15 -0
- novacode_cli/prompts/desktop_power.jinja +4 -0
- novacode_cli/prompts/file_paths.jinja +15 -0
- novacode_cli/prompts/file_tracker.jinja +23 -0
- novacode_cli/prompts/init_agents_md.jinja +56 -0
- novacode_cli/prompts/init_exploration.jinja +174 -0
- novacode_cli/prompts/init_semantic_orchestrate.jinja +37 -0
- novacode_cli/prompts/instruction_scope.jinja +5 -0
- novacode_cli/prompts/learnings_extraction.jinja +45 -0
- novacode_cli/prompts/longterm_memory.jinja +54 -0
- novacode_cli/prompts/mcp.jinja +84 -0
- novacode_cli/prompts/memory_consolidation.jinja +40 -0
- novacode_cli/prompts/nova_evolution.jinja +73 -0
- novacode_cli/prompts/nova_review.jinja +158 -0
- novacode_cli/prompts/nova_skill_debate.jinja +36 -0
- novacode_cli/prompts/nova_standing_instructions.jinja +26 -0
- novacode_cli/prompts/nova_verify.jinja +100 -0
- novacode_cli/prompts/onboard_dashboard.jinja +378 -0
- novacode_cli/prompts/plan_agent.jinja +87 -0
- novacode_cli/prompts/plan_scouts.jinja +5 -0
- novacode_cli/prompts/plan_storage.jinja +8 -0
- novacode_cli/prompts/planning.jinja +82 -0
- novacode_cli/prompts/ponytail/LICENSE +21 -0
- novacode_cli/prompts/ponytail/UPSTREAM.json +15 -0
- novacode_cli/prompts/ponytail/hooks/ponytail-config.js +175 -0
- novacode_cli/prompts/ponytail/hooks/ponytail-instructions.js +98 -0
- novacode_cli/prompts/ponytail/skills/ponytail/SKILL.md +52 -0
- novacode_cli/prompts/ponytail/skills/ponytail-audit/SKILL.md +103 -0
- novacode_cli/prompts/ponytail/skills/ponytail-debt/SKILL.md +44 -0
- novacode_cli/prompts/ponytail/skills/ponytail-gain/SKILL.md +52 -0
- novacode_cli/prompts/ponytail/skills/ponytail-help/SKILL.md +76 -0
- novacode_cli/prompts/ponytail/skills/ponytail-review/SKILL.md +101 -0
- novacode_cli/prompts/prompt_evolution.jinja +27 -0
- novacode_cli/prompts/ralph_iteration.jinja +75 -0
- novacode_cli/prompts/refine_gate.jinja +31 -0
- novacode_cli/prompts/refine_plan.jinja +64 -0
- novacode_cli/prompts/refine_review.jinja +34 -0
- novacode_cli/prompts/research_delivery.jinja +9 -0
- novacode_cli/prompts/research_swarm.jinja +130 -0
- novacode_cli/prompts/response_style.jinja +5 -0
- novacode_cli/prompts/session_summarization.jinja +86 -0
- novacode_cli/prompts/skill_creation.jinja +85 -0
- novacode_cli/prompts/subagents/fact_checker.jinja +48 -0
- novacode_cli/prompts/subagents/financial_analyst.jinja +50 -0
- novacode_cli/prompts/subagents/literature_reviewer.jinja +52 -0
- novacode_cli/prompts/subagents/market_analyst.jinja +48 -0
- novacode_cli/prompts/subagents/research_synthesizer.jinja +57 -0
- novacode_cli/prompts/subagents/technical_researcher.jinja +56 -0
- novacode_cli/prompts/subagents/web_researcher.jinja +46 -0
- novacode_cli/prompts/summarization.jinja +107 -0
- novacode_cli/prompts/summarization_answers.jinja +23 -0
- novacode_cli/prompts/summarization_questions.jinja +32 -0
- novacode_cli/prompts/vision_caption.jinja +21 -0
- novacode_cli/prompts/voice_summary.jinja +65 -0
- novacode_cli/prompts/wiki_file.jinja +57 -0
- novacode_cli/prompts/wiki_ingest.jinja +87 -0
- novacode_cli/recovery.py +383 -0
- novacode_cli/remote/__init__.py +45 -0
- novacode_cli/remote/approvals.py +282 -0
- novacode_cli/remote/async_task_clients.py +97 -0
- novacode_cli/remote/async_task_watcher.py +240 -0
- novacode_cli/remote/bridge.py +905 -0
- novacode_cli/remote/command_menu.py +26 -0
- novacode_cli/remote/command_ui.py +243 -0
- novacode_cli/remote/config.py +199 -0
- novacode_cli/remote/discord_bridge.py +530 -0
- novacode_cli/remote/discord_format.py +31 -0
- novacode_cli/remote/discord_threads.py +225 -0
- novacode_cli/remote/images.py +97 -0
- novacode_cli/remote/processor.py +601 -0
- novacode_cli/remote/rootapp_bot/bot.cjs +16 -0
- novacode_cli/remote/rootapp_bot/package-lock.json +2683 -0
- novacode_cli/remote/rootapp_bot/package.json +18 -0
- novacode_cli/remote/rootapp_bot/transport.cjs +80 -0
- novacode_cli/remote/rootapp_bridge.py +370 -0
- novacode_cli/remote/rootapp_setup.py +89 -0
- novacode_cli/remote/routing.py +103 -0
- novacode_cli/remote/scheduler.py +302 -0
- novacode_cli/remote/session.py +164 -0
- novacode_cli/remote/status.py +411 -0
- novacode_cli/remote/streaming.py +188 -0
- novacode_cli/remote/telegram_bridge.py +1118 -0
- novacode_cli/remote/telegram_format.py +215 -0
- novacode_cli/remote/telegram_hub.py +256 -0
- novacode_cli/remote/voice_notes.py +120 -0
- novacode_cli/remote/webhook_adapters.py +167 -0
- novacode_cli/remote/webhook_server.py +154 -0
- novacode_cli/rewind.py +150 -0
- novacode_cli/rooms/__init__.py +1 -0
- novacode_cli/rooms/bridge.py +86 -0
- novacode_cli/rooms/cli.py +231 -0
- novacode_cli/rooms/nova.py +227 -0
- novacode_cli/rooms/runtime.py +299 -0
- novacode_cli/rooms/store.py +538 -0
- novacode_cli/rooms/workspace.py +94 -0
- novacode_cli/security/__init__.py +29 -0
- novacode_cli/security/approval_context.py +45 -0
- novacode_cli/security/delegated_approval.py +68 -0
- novacode_cli/security/local_http.py +85 -0
- novacode_cli/security/middleware.py +87 -0
- novacode_cli/security/paths.py +35 -0
- novacode_cli/security/policy.py +535 -0
- novacode_cli/security/policy_writer.py +90 -0
- novacode_cli/security/prompt_injection.py +203 -0
- novacode_cli/security/remember.py +81 -0
- novacode_cli/security/rule_synthesis.py +116 -0
- novacode_cli/security/secret_files.py +61 -0
- novacode_cli/security/session_allow.py +114 -0
- novacode_cli/security/unicode_security.py +516 -0
- novacode_cli/security/validator.py +173 -0
- novacode_cli/server/__init__.py +9 -0
- novacode_cli/server/app.py +632 -0
- novacode_cli/server/event_adapter.py +189 -0
- novacode_cli/server/session_manager.py +90 -0
- novacode_cli/server_main.py +64 -0
- novacode_cli/server_runner/dev_server.py +530 -0
- novacode_cli/server_runner/test_runner.py +470 -0
- novacode_cli/session/__init__.py +1 -0
- novacode_cli/session/adapters/__init__.py +240 -0
- novacode_cli/session/adapters/claude_code.py +102 -0
- novacode_cli/session/adapters/codex.py +77 -0
- novacode_cli/session/adapters/harness_native.py +75 -0
- novacode_cli/session/checkpoint_coordinator.py +448 -0
- novacode_cli/session/context_restore.py +25 -0
- novacode_cli/session/history_export.py +228 -0
- novacode_cli/session/history_tree.py +174 -0
- novacode_cli/session/imported_context.py +267 -0
- novacode_cli/session/runtime_identity.py +32 -0
- novacode_cli/session/session_persistence.py +1168 -0
- novacode_cli/session/session_prompt_builder.py +279 -0
- novacode_cli/session/session_restore.py +351 -0
- novacode_cli/session/session_summarization.py +20 -0
- novacode_cli/session/transcript_journal.py +153 -0
- novacode_cli/sessions/__init__.py +11 -0
- novacode_cli/sessions/gateway.py +84 -0
- novacode_cli/sessions/launch.py +51 -0
- novacode_cli/sessions/lease.py +213 -0
- novacode_cli/sessions/messaging.py +255 -0
- novacode_cli/sessions/protocol.py +216 -0
- novacode_cli/sessions/resume.py +31 -0
- novacode_cli/sessions/supervisor.py +498 -0
- novacode_cli/sessions/tasks.py +73 -0
- novacode_cli/sessions/tui_messaging.py +413 -0
- novacode_cli/sessions/worker.py +652 -0
- novacode_cli/sessions/worktree.py +262 -0
- novacode_cli/shell/__init__.py +27 -0
- novacode_cli/shell/jobs.py +475 -0
- novacode_cli/shell/middleware.py +1568 -0
- novacode_cli/shell/os_sandbox.py +205 -0
- novacode_cli/shell/output.py +181 -0
- novacode_cli/shell/patterns.py +275 -0
- novacode_cli/shell/processes.py +87 -0
- novacode_cli/shell/utils.py +236 -0
- novacode_cli/skills/__init__.py +26 -0
- novacode_cli/skills/builtin/handoff/SKILL.md +15 -0
- novacode_cli/skills/curation_middleware.py +69 -0
- novacode_cli/skills/executable.py +161 -0
- novacode_cli/skills/libraries.py +102 -0
- novacode_cli/skills/load.py +248 -0
- novacode_cli/skills/prune.py +215 -0
- novacode_cli/skills/refreshing_middleware.py +896 -0
- novacode_cli/skills/retrieval.py +248 -0
- novacode_cli/skills/runtime.py +118 -0
- novacode_cli/skills/schema.py +254 -0
- novacode_cli/skills/skill_creation.py +1549 -0
- novacode_cli/skills/skill_lock.py +75 -0
- novacode_cli/skills/skill_system_prompt.py +16 -0
- novacode_cli/skills/skills_prefs.py +126 -0
- novacode_cli/skills/tool_resolver.py +31 -0
- novacode_cli/skills/upstream.py +50 -0
- novacode_cli/skills/versioning.py +291 -0
- novacode_cli/states/Session.py +847 -0
- novacode_cli/states/__init__.py +31 -0
- novacode_cli/states/slices/__init__.py +17 -0
- novacode_cli/states/slices/agent_runtime.py +249 -0
- novacode_cli/states/slices/background_tasks.py +21 -0
- novacode_cli/states/slices/notifications.py +74 -0
- novacode_cli/states/slices/remote_bridge.py +56 -0
- novacode_cli/states/slices/ui_settings.py +49 -0
- novacode_cli/states/slices/wiki.py +25 -0
- novacode_cli/teammates/__init__.py +1 -0
- novacode_cli/teammates/controller.py +557 -0
- novacode_cli/teammates/graph.py +104 -0
- novacode_cli/teammates/registry.py +175 -0
- novacode_cli/teammates/service.py +254 -0
- novacode_cli/teammates/worker.py +19 -0
- novacode_cli/teams/__init__.py +1 -0
- novacode_cli/teams/adapters/__init__.py +1 -0
- novacode_cli/teams/adapters/container.py +316 -0
- novacode_cli/teams/adapters/inprocess.py +206 -0
- novacode_cli/teams/adapters/process.py +364 -0
- novacode_cli/teams/bootstrap.py +246 -0
- novacode_cli/teams/container_git.py +67 -0
- novacode_cli/teams/context.py +34 -0
- novacode_cli/teams/coordinator.py +815 -0
- novacode_cli/teams/gateway.py +200 -0
- novacode_cli/teams/host.py +64 -0
- novacode_cli/teams/legacy.py +52 -0
- novacode_cli/teams/middleware.py +176 -0
- novacode_cli/teams/models.py +104 -0
- novacode_cli/teams/output.py +47 -0
- novacode_cli/teams/protocol.py +50 -0
- novacode_cli/teams/recovery.py +80 -0
- novacode_cli/teams/scheduler.py +44 -0
- novacode_cli/teams/store.py +631 -0
- novacode_cli/teams/worker.py +251 -0
- novacode_cli/teams/workspace.py +100 -0
- novacode_cli/token_utils.py +256 -0
- novacode_cli/tools/__init__.py +104 -0
- novacode_cli/tools/_shared.py +97 -0
- novacode_cli/tools/_types.py +24 -0
- novacode_cli/tools/artifact_tools.py +137 -0
- novacode_cli/tools/code_search_tools.py +330 -0
- novacode_cli/tools/computer_tools.py +77 -0
- novacode_cli/tools/cron_tools.py +65 -0
- novacode_cli/tools/daemon_tool.py +290 -0
- novacode_cli/tools/desktop_tools.py +161 -0
- novacode_cli/tools/fetch_tools.py +530 -0
- novacode_cli/tools/http_tools.py +279 -0
- novacode_cli/tools/inspection_tools.py +107 -0
- novacode_cli/tools/job_tools.py +143 -0
- novacode_cli/tools/knowledge_tools.py +427 -0
- novacode_cli/tools/memory_tools.py +461 -0
- novacode_cli/tools/memory_wiki_tools.py +40 -0
- novacode_cli/tools/notebook_tools.py +238 -0
- novacode_cli/tools/oracle_tool.py +83 -0
- novacode_cli/tools/plan_archive_tools.py +80 -0
- novacode_cli/tools/plan_mode_tools.py +315 -0
- novacode_cli/tools/python_kernel_loop.py +165 -0
- novacode_cli/tools/python_kernel_tool.py +222 -0
- novacode_cli/tools/reflection_tools.py +34 -0
- novacode_cli/tools/scraper_tools.py +377 -0
- novacode_cli/tools/skill_tools.py +275 -0
- novacode_cli/tools/speak_tool.py +51 -0
- novacode_cli/tools/store_memory_tools.py +227 -0
- novacode_cli/tools/ui_tools.py +62 -0
- novacode_cli/tools/waiter_tools.py +408 -0
- novacode_cli/tools/web_tools.py +349 -0
- novacode_cli/tools/wiki_tools.py +152 -0
- novacode_cli/tools/worktree_tools.py +219 -0
- novacode_cli/tracking/__init__.py +10 -0
- novacode_cli/tracking/file_tracker.py +826 -0
- novacode_cli/tracking/local_metrics.py +170 -0
- novacode_cli/tracking/loop_guard.py +507 -0
- novacode_cli/tracking/request_metrics.py +60 -0
- novacode_cli/tracking/tracing.py +692 -0
- novacode_cli/tracking/usage_tree.py +677 -0
- novacode_cli/tracking/workspace_anchoring.py +231 -0
- novacode_cli/tui/__init__.py +11 -0
- novacode_cli/tui/animation_rate.py +9 -0
- novacode_cli/tui/animations.py +332 -0
- novacode_cli/tui/app.py +19315 -0
- novacode_cli/tui/auth_display.py +87 -0
- novacode_cli/tui/auth_screens.py +826 -0
- novacode_cli/tui/avatar.py +294 -0
- novacode_cli/tui/avatar_frames.py +21 -0
- novacode_cli/tui/background_tasks.py +35 -0
- novacode_cli/tui/browser_settings_screen.py +70 -0
- novacode_cli/tui/caveman_screen.py +185 -0
- novacode_cli/tui/council_settings_screen.py +182 -0
- novacode_cli/tui/council_view.py +535 -0
- novacode_cli/tui/decision_settings.py +268 -0
- novacode_cli/tui/diff_preview.py +199 -0
- novacode_cli/tui/gc_tuning.py +63 -0
- novacode_cli/tui/handoff_screen.py +206 -0
- novacode_cli/tui/harness.py +244 -0
- novacode_cli/tui/history_tree_screen.py +179 -0
- novacode_cli/tui/home.py +328 -0
- novacode_cli/tui/home_scene.py +94 -0
- novacode_cli/tui/icons.py +81 -0
- novacode_cli/tui/interface_styles.tcss +327 -0
- novacode_cli/tui/matrix3.py +108 -0
- novacode_cli/tui/mcp_tools_screen.py +131 -0
- novacode_cli/tui/motion.py +178 -0
- novacode_cli/tui/output_body.py +136 -0
- novacode_cli/tui/output_buffer.py +59 -0
- novacode_cli/tui/palette.py +334 -0
- novacode_cli/tui/pickers.py +237 -0
- novacode_cli/tui/prime_panel.py +506 -0
- novacode_cli/tui/provider_signin.py +213 -0
- novacode_cli/tui/pty_shell.py +159 -0
- novacode_cli/tui/reference_screens.py +444 -0
- novacode_cli/tui/resume_picker.py +346 -0
- novacode_cli/tui/rooms.py +597 -0
- novacode_cli/tui/screens.py +7088 -0
- novacode_cli/tui/session_import.py +333 -0
- novacode_cli/tui/session_pane.py +242 -0
- novacode_cli/tui/session_tab.py +42 -0
- novacode_cli/tui/session_transcript.py +118 -0
- novacode_cli/tui/shell_output.py +7 -0
- novacode_cli/tui/stall_watch.py +174 -0
- novacode_cli/tui/statusline.py +40 -0
- novacode_cli/tui/statusline_screen.py +91 -0
- novacode_cli/tui/teammate_updates.py +162 -0
- novacode_cli/tui/teammates.py +548 -0
- novacode_cli/tui/terminal.py +289 -0
- novacode_cli/tui/terminal_view.py +235 -0
- novacode_cli/tui/themes.py +118 -0
- novacode_cli/tui/update_notice.py +84 -0
- novacode_cli/tui/voice_downloads.py +113 -0
- novacode_cli/tui/widgets.py +2038 -0
- novacode_cli/tui/windows_input.py +38 -0
- novacode_cli/tui/workspace_approval.py +165 -0
- novacode_cli/ui/council_web.py +113 -0
- novacode_cli/ui/council_workspace.py +72 -0
- novacode_cli/ui/cowork_web.py +41 -0
- novacode_cli/ui/diff_highlight.py +160 -0
- novacode_cli/ui/execution.py +1396 -0
- novacode_cli/ui/exit_summary.py +65 -0
- novacode_cli/ui/hitl_approval.py +196 -0
- novacode_cli/ui/input_preparation.py +2 -0
- novacode_cli/ui/interrupt_handlers.py +215 -0
- novacode_cli/ui/question_prompt.py +151 -0
- novacode_cli/ui/status_phrases.py +157 -0
- novacode_cli/ui/stream_preview.py +66 -0
- novacode_cli/ui/streaming.py +2 -0
- novacode_cli/ui/subagent_tracking.py +2 -0
- novacode_cli/ui/ui_elements.py +1585 -0
- novacode_cli/ui/web_workspace.py +244 -0
- novacode_cli/ui_events.py +299 -0
- novacode_cli/update_ui.py +243 -0
- novacode_cli/updates.py +489 -0
- novacode_cli/utils/backend_patches.py +399 -0
- novacode_cli/utils/backend_paths.py +467 -0
- novacode_cli/utils/lazy_providers.py +51 -0
- novacode_cli/utils/model_info.py +223 -0
- novacode_cli/utils/pdf_extraction.py +183 -0
- novacode_cli/utils/startup.py +29 -0
- novacode_cli/utils/tasks.py +42 -0
- novacode_cli/utils/windows.py +9 -0
- novacode_cli/vixie/hook_adapter.py +127 -0
- novacode_cli/vixie/server.py +375 -0
- novacode_cli/wiki/__init__.py +15 -0
- novacode_cli/wiki/ask.py +56 -0
- novacode_cli/wiki/file.py +71 -0
- novacode_cli/wiki/ingest.py +184 -0
- novacode_cli/wiki/manager.py +261 -0
- novacode_cli-1.2.0.dist-info/METADATA +271 -0
- novacode_cli-1.2.0.dist-info/RECORD +691 -0
- novacode_cli-1.2.0.dist-info/WHEEL +5 -0
- novacode_cli-1.2.0.dist-info/entry_points.txt +2 -0
- novacode_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
- novacode_cli-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""Background tasks + cooperative control for the foreground shell command.
|
|
2
|
+
|
|
3
|
+
Two things live here:
|
|
4
|
+
|
|
5
|
+
1. :class:`ForegroundControl` — the shell/execute tool runs its subprocess inside
|
|
6
|
+
a detached thread with its own event loop (``ShellMiddleware._run_async``), so
|
|
7
|
+
the TUI can't reach it via Textual task cancellation. While a foreground command
|
|
8
|
+
runs, the middleware publishes a control; the TUI sets its events from keybinds
|
|
9
|
+
(Esc → ``kill``, Ctrl+B → ``detach``) and the read loop polls them.
|
|
10
|
+
|
|
11
|
+
2. :class:`JobRegistry` — the background task manager. A command detached with
|
|
12
|
+
Ctrl+B (or launched in the background) becomes a :class:`BackgroundJob` with a
|
|
13
|
+
bounded live log buffer, a per-job terminate signal, and status transitions
|
|
14
|
+
(running → done | failed | terminated). Observers (the TUI indicator/panel and
|
|
15
|
+
the completion notifier) are fired reactively on every state change.
|
|
16
|
+
|
|
17
|
+
# ponytail: Esc / Ctrl+B act on EVERY running foreground command (one turn's
|
|
18
|
+
# parallel calls). Key controls by tool_call_id if a per-call action is ever needed.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import threading
|
|
25
|
+
import time
|
|
26
|
+
from collections import deque
|
|
27
|
+
from collections.abc import Callable, Iterable
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Foreground control (Esc kill / Ctrl+B detach)
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ForegroundControl:
|
|
37
|
+
"""Signals the middleware's foreground read loop polls each iteration."""
|
|
38
|
+
|
|
39
|
+
command: str
|
|
40
|
+
kill: threading.Event = field(default_factory=threading.Event)
|
|
41
|
+
detach: threading.Event = field(default_factory=threading.Event)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Every foreground command running right now. A turn's tool calls run
|
|
45
|
+
# concurrently, so there can be several: with a single slot the newest call
|
|
46
|
+
# overwrote the older ones' control, and Esc / Ctrl+B could no longer reach them
|
|
47
|
+
# (an older command then ran to its full timeout: the original freeze).
|
|
48
|
+
_live: list[ForegroundControl] = []
|
|
49
|
+
_lock = threading.Lock()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def set_current(command: str) -> ForegroundControl:
|
|
53
|
+
"""Publish a fresh control for a foreground command about to run."""
|
|
54
|
+
ctl = ForegroundControl(command=command)
|
|
55
|
+
with _lock:
|
|
56
|
+
_live.append(ctl)
|
|
57
|
+
return ctl
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_current() -> ForegroundControl | None:
|
|
61
|
+
"""The most recently started live control, or None if nothing is running."""
|
|
62
|
+
with _lock:
|
|
63
|
+
return _live[-1] if _live else None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def clear_current(ctl: ForegroundControl) -> None:
|
|
67
|
+
"""Retire ``ctl`` once its command is done."""
|
|
68
|
+
with _lock:
|
|
69
|
+
if ctl in _live:
|
|
70
|
+
_live.remove(ctl)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def request_kill() -> bool:
|
|
74
|
+
"""Ask every running foreground command to die. True if any was running."""
|
|
75
|
+
with _lock:
|
|
76
|
+
live = list(_live)
|
|
77
|
+
for ctl in live:
|
|
78
|
+
ctl.kill.set()
|
|
79
|
+
return bool(live)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def request_detach() -> bool:
|
|
83
|
+
"""Move every running foreground command to the background.
|
|
84
|
+
|
|
85
|
+
True if at least one was running and not already being killed.
|
|
86
|
+
"""
|
|
87
|
+
with _lock:
|
|
88
|
+
live = [c for c in _live if not c.kill.is_set() and not c.detach.is_set()]
|
|
89
|
+
for ctl in live:
|
|
90
|
+
ctl.detach.set()
|
|
91
|
+
return bool(live)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Background jobs — the task manager
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
_LOG_MAXLEN = 1000
|
|
99
|
+
_LOG_MAX_CHARS = 200_000
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class BoundedLog(deque[str]):
|
|
103
|
+
"""Keep a job's recent output bounded by both chunks and characters."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, chunks: Iterable[str] = ()) -> None:
|
|
106
|
+
"""Load initial chunks through the same size limit as later writes."""
|
|
107
|
+
super().__init__()
|
|
108
|
+
self._chars = 0
|
|
109
|
+
self.total = 0
|
|
110
|
+
"""Characters written since the job started; a ``from_offset`` cursor counts these."""
|
|
111
|
+
self.extend(chunks)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def start(self) -> int:
|
|
115
|
+
"""Stream offset of the oldest character still retained."""
|
|
116
|
+
return self.total - self._chars
|
|
117
|
+
|
|
118
|
+
def append(self, chunk: str) -> None:
|
|
119
|
+
"""Retain the latest part of a large write without growing with its size."""
|
|
120
|
+
self.total += len(chunk)
|
|
121
|
+
chunk = chunk[-_LOG_MAX_CHARS:]
|
|
122
|
+
super().append(chunk)
|
|
123
|
+
self._chars += len(chunk)
|
|
124
|
+
while len(self) > _LOG_MAXLEN or self._chars > _LOG_MAX_CHARS:
|
|
125
|
+
self._chars -= len(super().popleft())
|
|
126
|
+
|
|
127
|
+
def extend(self, chunks: Iterable[str]) -> None:
|
|
128
|
+
"""Apply the same bound to bulk writes used when jobs detach."""
|
|
129
|
+
for chunk in chunks:
|
|
130
|
+
self.append(chunk)
|
|
131
|
+
|
|
132
|
+
# Maximum number of completed (non-running) jobs retained in the registry.
|
|
133
|
+
# Completed jobs are kept so ``/jobs`` can list and restart recent ones, but an
|
|
134
|
+
# unbounded dict leaks memory across a long session. Oldest completed jobs are
|
|
135
|
+
# evicted when a new job is added.
|
|
136
|
+
_MAX_COMPLETED_JOBS = 50
|
|
137
|
+
|
|
138
|
+
# event ∈ {started, output, completed, failed, terminated, cleared}; job may be
|
|
139
|
+
# None for "cleared".
|
|
140
|
+
Observer = Callable[[str, "BackgroundJob | None"], None]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def fmt_runtime(seconds: float) -> str:
|
|
144
|
+
"""Format a runtime as MM:SS (or H:MM:SS past an hour)."""
|
|
145
|
+
s = int(max(0, seconds))
|
|
146
|
+
h, rem = divmod(s, 3600)
|
|
147
|
+
m, sec = divmod(rem, 60)
|
|
148
|
+
return f"{h}:{m:02d}:{sec:02d}" if h else f"{m:02d}:{sec:02d}"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class BackgroundJob:
|
|
153
|
+
"""A shell/execute command running (or finished) in the background."""
|
|
154
|
+
|
|
155
|
+
id: int
|
|
156
|
+
command: str
|
|
157
|
+
tool_name: str
|
|
158
|
+
started_at: float
|
|
159
|
+
prog: list[str] | None = None
|
|
160
|
+
status: str = "running" # running | done | failed | terminated
|
|
161
|
+
exit_code: int | None = None
|
|
162
|
+
finished_at: float | None = None
|
|
163
|
+
pid: int | None = None
|
|
164
|
+
resume_on_done: bool = False
|
|
165
|
+
"""True for Ctrl+B-detached jobs: the agent was mid-task, so when this
|
|
166
|
+
finishes the TUI auto-resumes the agent with the result (vs. just notifying
|
|
167
|
+
for tasks the user explicitly launched/restarted)."""
|
|
168
|
+
agent_launched: bool = False
|
|
169
|
+
"""True when the AGENT started this job (``background=True`` on shell/bash/
|
|
170
|
+
execute), as opposed to the user launching or restarting it.
|
|
171
|
+
|
|
172
|
+
Such a job is part of work the agent is actively doing, so it should not sit
|
|
173
|
+
silently until the user happens to type again — the monitor watches these
|
|
174
|
+
and reports the result back into the conversation. A user-launched dev
|
|
175
|
+
server, by contrast, is expected to just keep running."""
|
|
176
|
+
logs: BoundedLog = field(default_factory=BoundedLog, repr=False)
|
|
177
|
+
kill: threading.Event = field(default_factory=threading.Event, repr=False)
|
|
178
|
+
_done: threading.Event = field(default_factory=threading.Event, repr=False)
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def task_id(self) -> str:
|
|
182
|
+
return f"task_{self.id}"
|
|
183
|
+
|
|
184
|
+
def runtime(self) -> float:
|
|
185
|
+
end = self.finished_at if self.finished_at is not None else time.time()
|
|
186
|
+
return max(0.0, end - self.started_at)
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def output(self) -> str:
|
|
190
|
+
return "".join(self.logs)
|
|
191
|
+
|
|
192
|
+
def status_glyph(self) -> str:
|
|
193
|
+
return {"running": "●", "done": "✓", "failed": "✗", "terminated": "◼"}.get(self.status, "●")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class JobRegistry:
|
|
197
|
+
"""Process-global background task manager."""
|
|
198
|
+
|
|
199
|
+
def __init__(self) -> None:
|
|
200
|
+
self._jobs: dict[int, BackgroundJob] = {}
|
|
201
|
+
self._next_id = 41 # cosmetic: task ids start at task_41 like the spec
|
|
202
|
+
self._lock = threading.Lock()
|
|
203
|
+
self._observers: list[Observer] = []
|
|
204
|
+
self._completion_cb: Callable[[BackgroundJob], None] | None = None
|
|
205
|
+
self._launcher: Callable[[str, list | None], BackgroundJob | None] | None = None
|
|
206
|
+
|
|
207
|
+
# -- lifecycle ---------------------------------------------------------
|
|
208
|
+
def add(self, command: str, tool_name: str, prog: list | None = None) -> BackgroundJob:
|
|
209
|
+
with self._lock:
|
|
210
|
+
job_id = self._next_id
|
|
211
|
+
self._next_id += 1
|
|
212
|
+
job = BackgroundJob(
|
|
213
|
+
id=job_id,
|
|
214
|
+
command=command,
|
|
215
|
+
tool_name=tool_name,
|
|
216
|
+
started_at=time.time(),
|
|
217
|
+
prog=list(prog) if prog else None,
|
|
218
|
+
)
|
|
219
|
+
self._jobs[job_id] = job
|
|
220
|
+
self._trim_completed_locked()
|
|
221
|
+
self._emit("started", job)
|
|
222
|
+
return job
|
|
223
|
+
|
|
224
|
+
def _trim_completed_locked(self) -> None:
|
|
225
|
+
"""Drop the oldest completed jobs so ``_jobs`` stays bounded.
|
|
226
|
+
|
|
227
|
+
Completed jobs are kept so ``/jobs`` can list and restart recent ones,
|
|
228
|
+
but an unbounded dict leaks memory across a long session. Called under
|
|
229
|
+
``self._lock`` on each new job; running jobs are never evicted.
|
|
230
|
+
"""
|
|
231
|
+
completed = [jid for jid, j in self._jobs.items() if j.status != "running"]
|
|
232
|
+
if len(completed) <= _MAX_COMPLETED_JOBS:
|
|
233
|
+
return
|
|
234
|
+
# Evict oldest first (job ids increase monotonically).
|
|
235
|
+
for jid in sorted(completed)[: len(completed) - _MAX_COMPLETED_JOBS]:
|
|
236
|
+
del self._jobs[jid]
|
|
237
|
+
|
|
238
|
+
def append_log(self, job_id: int, text: str) -> None:
|
|
239
|
+
job = self.get(job_id)
|
|
240
|
+
if job is None:
|
|
241
|
+
return
|
|
242
|
+
job.logs.append(text)
|
|
243
|
+
self._emit("output", job)
|
|
244
|
+
|
|
245
|
+
def attach_pid(self, job_id: int, pid: int | None) -> None:
|
|
246
|
+
job = self.get(job_id)
|
|
247
|
+
if job is not None:
|
|
248
|
+
job.pid = pid
|
|
249
|
+
|
|
250
|
+
def complete(self, job_id: int, exit_code: int | None, output: str | None = None) -> None:
|
|
251
|
+
with self._lock:
|
|
252
|
+
job = self._jobs.get(job_id)
|
|
253
|
+
if job is None or job.status != "running":
|
|
254
|
+
return
|
|
255
|
+
if output is not None:
|
|
256
|
+
job.logs.append(output)
|
|
257
|
+
job.exit_code = exit_code
|
|
258
|
+
job.status = "done" if exit_code in (0, None) else "failed"
|
|
259
|
+
job.finished_at = time.time()
|
|
260
|
+
job._done.set()
|
|
261
|
+
self._emit("completed" if job.status == "done" else "failed", job)
|
|
262
|
+
self._fire_completion(job)
|
|
263
|
+
|
|
264
|
+
def mark_terminated(self, job_id: int, exit_code: int | None = None) -> None:
|
|
265
|
+
with self._lock:
|
|
266
|
+
job = self._jobs.get(job_id)
|
|
267
|
+
if job is None or job.status != "running":
|
|
268
|
+
return
|
|
269
|
+
job.status = "terminated"
|
|
270
|
+
job.exit_code = exit_code
|
|
271
|
+
job.finished_at = time.time()
|
|
272
|
+
job._done.set()
|
|
273
|
+
self._emit("terminated", job)
|
|
274
|
+
self._fire_completion(job)
|
|
275
|
+
|
|
276
|
+
# -- actions -----------------------------------------------------------
|
|
277
|
+
def terminate(self, job_id: int) -> bool:
|
|
278
|
+
"""Signal a running job to stop (the drain loop terminates its process
|
|
279
|
+
tree gracefully, then marks it terminated). True if it was running."""
|
|
280
|
+
job = self.get(job_id)
|
|
281
|
+
if job is None or job.status != "running":
|
|
282
|
+
return False
|
|
283
|
+
job.kill.set()
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
def restart(self, job_id: int) -> BackgroundJob | None:
|
|
287
|
+
"""Re-run a job's command as a new background job (via the launcher)."""
|
|
288
|
+
job = self.get(job_id)
|
|
289
|
+
if job is None or self._launcher is None:
|
|
290
|
+
return None
|
|
291
|
+
return self._launcher(job.command, job.prog)
|
|
292
|
+
|
|
293
|
+
def clear_completed(self) -> int:
|
|
294
|
+
with self._lock:
|
|
295
|
+
done = [jid for jid, j in self._jobs.items() if j.status != "running"]
|
|
296
|
+
for jid in done:
|
|
297
|
+
del self._jobs[jid]
|
|
298
|
+
if done:
|
|
299
|
+
self._emit("cleared", None)
|
|
300
|
+
return len(done)
|
|
301
|
+
|
|
302
|
+
# -- queries -----------------------------------------------------------
|
|
303
|
+
def get(self, job_id: int) -> BackgroundJob | None:
|
|
304
|
+
with self._lock:
|
|
305
|
+
return self._jobs.get(job_id)
|
|
306
|
+
|
|
307
|
+
def resolve(self, ref: str | int) -> BackgroundJob | None:
|
|
308
|
+
"""Look up a job by numeric id, ``"task_42"``, or ``"42"``."""
|
|
309
|
+
try:
|
|
310
|
+
jid = int(str(ref).lower().replace("task_", "").strip())
|
|
311
|
+
except (ValueError, TypeError):
|
|
312
|
+
return None
|
|
313
|
+
return self.get(jid)
|
|
314
|
+
|
|
315
|
+
def list_jobs(self) -> list[BackgroundJob]:
|
|
316
|
+
with self._lock:
|
|
317
|
+
return list(self._jobs.values())
|
|
318
|
+
|
|
319
|
+
def active(self) -> list[BackgroundJob]:
|
|
320
|
+
with self._lock:
|
|
321
|
+
return [j for j in self._jobs.values() if j.status == "running"]
|
|
322
|
+
|
|
323
|
+
def active_count(self) -> int:
|
|
324
|
+
return len(self.active())
|
|
325
|
+
|
|
326
|
+
def wait(self, job_id: int, timeout: float | None = None) -> BackgroundJob | None:
|
|
327
|
+
job = self.get(job_id)
|
|
328
|
+
if job is None:
|
|
329
|
+
return None
|
|
330
|
+
job._done.wait(timeout)
|
|
331
|
+
return job
|
|
332
|
+
|
|
333
|
+
# -- observers ---------------------------------------------------------
|
|
334
|
+
def add_observer(self, cb: Observer) -> None:
|
|
335
|
+
if cb not in self._observers:
|
|
336
|
+
self._observers.append(cb)
|
|
337
|
+
|
|
338
|
+
def remove_observer(self, cb: Observer) -> None:
|
|
339
|
+
"""Detach an observer when its UI/session owner shuts down."""
|
|
340
|
+
self._observers = [observer for observer in self._observers if observer != cb]
|
|
341
|
+
|
|
342
|
+
def set_launcher(self, fn: Callable[[str, list | None], BackgroundJob | None]) -> None:
|
|
343
|
+
self._launcher = fn
|
|
344
|
+
|
|
345
|
+
def set_completion_callback(self, cb: Callable[[BackgroundJob], None] | None) -> None:
|
|
346
|
+
"""Back-compat: called once per job on a terminal transition."""
|
|
347
|
+
self._completion_cb = cb
|
|
348
|
+
|
|
349
|
+
def _emit(self, event: str, job: BackgroundJob | None) -> None:
|
|
350
|
+
for cb in list(self._observers):
|
|
351
|
+
try:
|
|
352
|
+
cb(event, job)
|
|
353
|
+
except Exception: # noqa: BLE001 — a bad observer must not wedge a job
|
|
354
|
+
pass
|
|
355
|
+
|
|
356
|
+
def _fire_completion(self, job: BackgroundJob) -> None:
|
|
357
|
+
cb = self._completion_cb
|
|
358
|
+
if cb is not None:
|
|
359
|
+
try:
|
|
360
|
+
cb(job)
|
|
361
|
+
except Exception: # noqa: BLE001
|
|
362
|
+
pass
|
|
363
|
+
|
|
364
|
+
def reset(self) -> None:
|
|
365
|
+
"""Drop all jobs, observers, and the completion callback (test isolation)."""
|
|
366
|
+
with self._lock:
|
|
367
|
+
self._jobs.clear()
|
|
368
|
+
self._next_id = 41
|
|
369
|
+
self._observers.clear()
|
|
370
|
+
self._completion_cb = None
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
_registry: JobRegistry | None = None
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def get_registry() -> JobRegistry:
|
|
377
|
+
"""Return the process-global background-task registry."""
|
|
378
|
+
global _registry
|
|
379
|
+
if _registry is None:
|
|
380
|
+
_registry = JobRegistry()
|
|
381
|
+
_register_exit_cleanup()
|
|
382
|
+
return _registry
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _kill_pid_tree(pid: int) -> None:
|
|
386
|
+
"""Force-kill a process and its children by pid (sync; for atexit)."""
|
|
387
|
+
from novacode_cli.shell.processes import kill_process_tree
|
|
388
|
+
|
|
389
|
+
kill_process_tree(pid)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
_exit_cleanup_registered = False
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _register_exit_cleanup() -> None:
|
|
396
|
+
"""Kill any still-running background task trees when the process exits, so a
|
|
397
|
+
detached `npm run dev` doesn't outlive the assistant (spec: clean up on exit)."""
|
|
398
|
+
global _exit_cleanup_registered
|
|
399
|
+
if _exit_cleanup_registered:
|
|
400
|
+
return
|
|
401
|
+
_exit_cleanup_registered = True
|
|
402
|
+
import atexit
|
|
403
|
+
|
|
404
|
+
def _cleanup() -> None:
|
|
405
|
+
reg = _registry
|
|
406
|
+
if reg is None:
|
|
407
|
+
return
|
|
408
|
+
for job in reg.active():
|
|
409
|
+
if job.pid:
|
|
410
|
+
_kill_pid_tree(job.pid)
|
|
411
|
+
|
|
412
|
+
atexit.register(_cleanup)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# ---------------------------------------------------------------------------
|
|
416
|
+
# Shared background event loop — lets a detached command's drain outlive the
|
|
417
|
+
# synchronous tool call that started it (see the middleware for the full why).
|
|
418
|
+
# ---------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
_bg_loop: asyncio.AbstractEventLoop | None = None
|
|
421
|
+
_bg_loop_lock = threading.Lock()
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def get_background_loop() -> asyncio.AbstractEventLoop:
|
|
425
|
+
"""Return the shared background event loop, starting it on first use.
|
|
426
|
+
|
|
427
|
+
Runs in a daemon thread (dies with the process). On Windows the default
|
|
428
|
+
policy yields a Proactor loop, which supports subprocesses.
|
|
429
|
+
"""
|
|
430
|
+
global _bg_loop
|
|
431
|
+
with _bg_loop_lock:
|
|
432
|
+
if _bg_loop is None or _bg_loop.is_closed():
|
|
433
|
+
loop = asyncio.new_event_loop()
|
|
434
|
+
threading.Thread(
|
|
435
|
+
target=loop.run_forever, name="nova-shell-bg-loop", daemon=True
|
|
436
|
+
).start()
|
|
437
|
+
_bg_loop = loop
|
|
438
|
+
return _bg_loop
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
if __name__ == "__main__":
|
|
442
|
+
# ponytail: self-check for the foreground slot + registry state machine.
|
|
443
|
+
assert get_current() is None and request_kill() is False
|
|
444
|
+
c = set_current("sleep 100")
|
|
445
|
+
assert request_kill() is True and c.kill.is_set()
|
|
446
|
+
assert request_detach() is False # kill wins
|
|
447
|
+
clear_current(c)
|
|
448
|
+
|
|
449
|
+
r = JobRegistry()
|
|
450
|
+
events: list[tuple[str, str | None]] = []
|
|
451
|
+
r.add_observer(lambda ev, j: events.append((ev, j.task_id if j else None)))
|
|
452
|
+
j = r.add("npm run dev", "shell")
|
|
453
|
+
assert j.task_id == "task_41" and j.status == "running"
|
|
454
|
+
r.append_log(j.id, "listening on :3000\n")
|
|
455
|
+
assert "listening" in j.output
|
|
456
|
+
assert r.resolve("task_41") is j and r.resolve(41) is j and r.resolve("nope") is None
|
|
457
|
+
assert r.terminate(j.id) is True and j.kill.is_set()
|
|
458
|
+
r.mark_terminated(j.id, exit_code=-15)
|
|
459
|
+
assert j.status == "terminated"
|
|
460
|
+
assert r.terminate(j.id) is False # not running
|
|
461
|
+
|
|
462
|
+
j2 = r.add("pytest", "shell")
|
|
463
|
+
r.complete(j2.id, 1)
|
|
464
|
+
assert j2.status == "failed" and j2.exit_code == 1
|
|
465
|
+
j3 = r.add("build", "shell")
|
|
466
|
+
r.complete(j3.id, 0)
|
|
467
|
+
assert j3.status == "done"
|
|
468
|
+
assert r.active_count() == 0
|
|
469
|
+
assert r.clear_completed() == 3 and r.list_jobs() == []
|
|
470
|
+
assert [e[0] for e in events] == [
|
|
471
|
+
"started", "output", "terminated",
|
|
472
|
+
"started", "failed", "started", "completed", "cleared",
|
|
473
|
+
]
|
|
474
|
+
assert fmt_runtime(151) == "02:31" and fmt_runtime(3661) == "1:01:01"
|
|
475
|
+
print("shell.jobs self-check ok")
|