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,565 @@
|
|
|
1
|
+
"""Durable host decisions, usage reservations and independent worker attempts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from copy import deepcopy
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from novacode_cli.states.Session import SessionState
|
|
15
|
+
|
|
16
|
+
from novacode_cli.prime_factory import DEFAULT_OUTPUT_LIMIT, ROLES, VERSION
|
|
17
|
+
from novacode_cli.prime_factory.state import bounded_text, check_definition, digest, plan_definition
|
|
18
|
+
from novacode_cli.prime_factory.store import FactoryError, FactoryStore
|
|
19
|
+
from novacode_cli.prime_memory import project_context, safe_text
|
|
20
|
+
|
|
21
|
+
ACTIVE = {"running", "starting", "uncertain"}
|
|
22
|
+
MAX_ACTIVE_SECONDS = 86400
|
|
23
|
+
MAX_SPECIALISTS = 3
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def storage_path(root: Path) -> Path:
|
|
27
|
+
"""Share state across worktrees without writing into a tracked checkout."""
|
|
28
|
+
context = project_context(str(root))
|
|
29
|
+
return Path(context.get("common_dir") or root / ".nova") / "prime_factory" / "runs.sqlite3"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FactoryController:
|
|
33
|
+
"""Only host code can approve stages, certify checks or authorize publication."""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self, store: FactoryStore, record: dict[str, Any], session: SessionState | None = None
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Bind one optimistic durable owner and its originating session."""
|
|
39
|
+
self.store, self.record, self.session = store, record, session
|
|
40
|
+
self.lock = asyncio.Lock()
|
|
41
|
+
self.handles: dict[str, Any] = {}
|
|
42
|
+
self.events: asyncio.Queue[Any] = asyncio.Queue()
|
|
43
|
+
self._clock = time.monotonic()
|
|
44
|
+
self._active = record["status"] == "running"
|
|
45
|
+
self._storage_failed = False
|
|
46
|
+
self._waiting: set[str] = set()
|
|
47
|
+
self._approval_epoch = 0
|
|
48
|
+
self._pending_approvals: set[asyncio.Future[Any]] = set()
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def create(
|
|
52
|
+
cls,
|
|
53
|
+
root: Path,
|
|
54
|
+
goal: str,
|
|
55
|
+
thread: str,
|
|
56
|
+
routes: dict[str, Any],
|
|
57
|
+
*,
|
|
58
|
+
session: SessionState | None = None,
|
|
59
|
+
store: FactoryStore | None = None,
|
|
60
|
+
) -> FactoryController:
|
|
61
|
+
"""Persist intent before the first model call or workspace mutation."""
|
|
62
|
+
record = {
|
|
63
|
+
"version": VERSION,
|
|
64
|
+
"factory": True,
|
|
65
|
+
"id": uuid.uuid4().hex,
|
|
66
|
+
"thread": thread,
|
|
67
|
+
"root": str(root.resolve()),
|
|
68
|
+
"goal": bounded_text(goal),
|
|
69
|
+
"stage": "requirements",
|
|
70
|
+
"phase": "requirements",
|
|
71
|
+
"status": "running",
|
|
72
|
+
"criteria": [],
|
|
73
|
+
"tasks": [],
|
|
74
|
+
"sources": [],
|
|
75
|
+
"checks": [],
|
|
76
|
+
"attempts": [],
|
|
77
|
+
"approvals": {},
|
|
78
|
+
"blockers": [],
|
|
79
|
+
"routes": routes,
|
|
80
|
+
"output_token_limit": DEFAULT_OUTPUT_LIMIT,
|
|
81
|
+
"reservations": {},
|
|
82
|
+
"budget": {"tokens": 500_000, "active_seconds": 3600.0},
|
|
83
|
+
"usage": {"tokens": 0, "active_seconds": 0.0, "by_role": {}},
|
|
84
|
+
"candidate": None,
|
|
85
|
+
"workspace": None,
|
|
86
|
+
"isolation": "worktree",
|
|
87
|
+
"baseline_selection": None,
|
|
88
|
+
"publication": None,
|
|
89
|
+
}
|
|
90
|
+
controller = cls(store or FactoryStore(storage_path(root)), record, session)
|
|
91
|
+
controller.save()
|
|
92
|
+
return controller
|
|
93
|
+
|
|
94
|
+
def tick(self) -> None:
|
|
95
|
+
"""Count active elapsed time once across all workers, never per worker."""
|
|
96
|
+
with self.store.lock:
|
|
97
|
+
now = time.monotonic()
|
|
98
|
+
workers = {a["id"] for a in self.record["attempts"] if a["status"] in ACTIVE}
|
|
99
|
+
all_waiting = bool(self._waiting) and workers <= self._waiting
|
|
100
|
+
if self._active and not all_waiting:
|
|
101
|
+
self.record["usage"]["active_seconds"] += max(0, now - self._clock)
|
|
102
|
+
self._clock = now
|
|
103
|
+
|
|
104
|
+
def begin_wait(self, key: str) -> None:
|
|
105
|
+
"""Exclude human wait time only while every active worker is waiting."""
|
|
106
|
+
with self.store.lock:
|
|
107
|
+
self.tick()
|
|
108
|
+
self._waiting.add(key)
|
|
109
|
+
|
|
110
|
+
def end_wait(self, key: str) -> None:
|
|
111
|
+
"""Finish a host-owned wait without charging the preceding idle interval."""
|
|
112
|
+
with self.store.lock:
|
|
113
|
+
self.tick()
|
|
114
|
+
self._waiting.discard(key)
|
|
115
|
+
|
|
116
|
+
def save(self) -> None:
|
|
117
|
+
"""Fail closed on storage errors; no caller may dispatch after a failed save."""
|
|
118
|
+
with self.store.lock:
|
|
119
|
+
if self._storage_failed:
|
|
120
|
+
msg = "Factory storage failed; further mutations are blocked"
|
|
121
|
+
raise FactoryError(msg)
|
|
122
|
+
self.tick()
|
|
123
|
+
try:
|
|
124
|
+
self.store.save(self.record)
|
|
125
|
+
except Exception as exc:
|
|
126
|
+
self._storage_failed = True
|
|
127
|
+
if self.record["status"] not in {"cancelled", "published"}:
|
|
128
|
+
self.record["status"] = "paused"
|
|
129
|
+
self._active = False
|
|
130
|
+
msg = "Factory storage failed; further mutations are blocked"
|
|
131
|
+
raise FactoryError(msg) from exc
|
|
132
|
+
if (
|
|
133
|
+
self.session is not None
|
|
134
|
+
and getattr(self.session, "thread_id", self.record["thread"])
|
|
135
|
+
== self.record["thread"]
|
|
136
|
+
):
|
|
137
|
+
self.session.prime_report = self.progress()
|
|
138
|
+
|
|
139
|
+
def progress(self) -> dict[str, Any]:
|
|
140
|
+
"""Return a detached dashboard snapshot without raw worker transcripts."""
|
|
141
|
+
snapshot = {
|
|
142
|
+
key: deepcopy(value)
|
|
143
|
+
for key, value in self.record.items()
|
|
144
|
+
if key not in {"candidate", "attempts", "checks", "sources", "publication"}
|
|
145
|
+
}
|
|
146
|
+
candidate = self.record.get("candidate")
|
|
147
|
+
snapshot["candidate"] = (
|
|
148
|
+
{
|
|
149
|
+
key: candidate[key]
|
|
150
|
+
for key in ("id", "revision", "branch", "workspace")
|
|
151
|
+
if key in candidate
|
|
152
|
+
}
|
|
153
|
+
if candidate
|
|
154
|
+
else None
|
|
155
|
+
)
|
|
156
|
+
snapshot["attempts"] = [
|
|
157
|
+
{
|
|
158
|
+
key: deepcopy(value)
|
|
159
|
+
for key, value in attempt.items()
|
|
160
|
+
if key not in {"result", "output", "recovery"}
|
|
161
|
+
}
|
|
162
|
+
for attempt in self.record["attempts"][-64:]
|
|
163
|
+
]
|
|
164
|
+
snapshot["checks"] = [
|
|
165
|
+
{
|
|
166
|
+
**{key: deepcopy(value) for key, value in check.items() if key != "evidence"},
|
|
167
|
+
"evidence_count": len(check["evidence"]),
|
|
168
|
+
"evidence": [
|
|
169
|
+
{key: value for key, value in evidence.items() if key != "output"}
|
|
170
|
+
for evidence in check["evidence"][-1:]
|
|
171
|
+
],
|
|
172
|
+
}
|
|
173
|
+
for check in self.record["checks"]
|
|
174
|
+
]
|
|
175
|
+
snapshot["sources"] = [
|
|
176
|
+
{
|
|
177
|
+
"id": source["id"],
|
|
178
|
+
"content": {
|
|
179
|
+
key: value
|
|
180
|
+
for key, value in source["content"].items()
|
|
181
|
+
if key not in {"excerpt", "decisions"}
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
for source in self.record["sources"]
|
|
185
|
+
]
|
|
186
|
+
publication = self.record.get("publication")
|
|
187
|
+
snapshot["publication"] = (
|
|
188
|
+
{
|
|
189
|
+
key: publication[key]
|
|
190
|
+
for key in ("id", "status", "url", "destination", "branch", "base")
|
|
191
|
+
if key in publication
|
|
192
|
+
}
|
|
193
|
+
if publication
|
|
194
|
+
else None
|
|
195
|
+
)
|
|
196
|
+
return snapshot
|
|
197
|
+
|
|
198
|
+
def _invalidate_approvals(self) -> None:
|
|
199
|
+
self._approval_epoch += 1
|
|
200
|
+
for future in tuple(self._pending_approvals):
|
|
201
|
+
|
|
202
|
+
def reject(target: asyncio.Future[Any] = future) -> None:
|
|
203
|
+
if not target.done():
|
|
204
|
+
target.set_result({"response": {"approved": False}})
|
|
205
|
+
|
|
206
|
+
if not future.get_loop().is_closed():
|
|
207
|
+
future.get_loop().call_soon_threadsafe(reject)
|
|
208
|
+
|
|
209
|
+
def request_pause(self) -> None:
|
|
210
|
+
"""Fence new work immediately; the driver stops owned workers at its next boundary."""
|
|
211
|
+
with self.store.lock:
|
|
212
|
+
self.tick()
|
|
213
|
+
self._active = False
|
|
214
|
+
self._invalidate_approvals()
|
|
215
|
+
if self.record["status"] not in {"cancelled", "published"}:
|
|
216
|
+
self.record["status"] = "paused"
|
|
217
|
+
if not self._storage_failed:
|
|
218
|
+
self.save()
|
|
219
|
+
|
|
220
|
+
def request_cancel(self) -> None:
|
|
221
|
+
"""Preserve unfinished branches and prevent subsequent resume dispatch."""
|
|
222
|
+
with self.store.lock:
|
|
223
|
+
if self.record["status"] == "published":
|
|
224
|
+
msg = "Published runs cannot be cancelled"
|
|
225
|
+
raise FactoryError(msg)
|
|
226
|
+
self.tick()
|
|
227
|
+
self._active = False
|
|
228
|
+
self._invalidate_approvals()
|
|
229
|
+
self.record["status"] = "cancelled"
|
|
230
|
+
if not self._storage_failed:
|
|
231
|
+
self.save()
|
|
232
|
+
|
|
233
|
+
def set_budget(self, tokens: int, active_seconds: float) -> None:
|
|
234
|
+
"""A human budget change invalidates plan and delivery approval."""
|
|
235
|
+
if type(tokens) is not int or tokens <= 0 or not 0 < active_seconds <= MAX_ACTIVE_SECONDS:
|
|
236
|
+
msg = "Budget requires positive tokens and at most 1440 active minutes"
|
|
237
|
+
raise FactoryError(msg)
|
|
238
|
+
self.record["budget"] = {"tokens": tokens, "active_seconds": active_seconds}
|
|
239
|
+
self.record["approvals"].clear()
|
|
240
|
+
self.request_pause()
|
|
241
|
+
|
|
242
|
+
def ensure_active(self) -> None:
|
|
243
|
+
"""Check shared limits immediately before every dispatch and mutation."""
|
|
244
|
+
if (
|
|
245
|
+
self.session is not None
|
|
246
|
+
and getattr(self.session, "thread_id", self.record["thread"]) != self.record["thread"]
|
|
247
|
+
):
|
|
248
|
+
self.request_pause()
|
|
249
|
+
message = "Conversation changed; the factory is paused until explicitly resumed"
|
|
250
|
+
raise FactoryError(message)
|
|
251
|
+
self.tick()
|
|
252
|
+
if self._storage_failed or self.record["status"] != "running":
|
|
253
|
+
msg = "Factory is suspended; explicit resume is required"
|
|
254
|
+
raise FactoryError(msg)
|
|
255
|
+
usage, budget = self.record["usage"], self.record["budget"]
|
|
256
|
+
reserved = sum(r["tokens"] for r in self.record["reservations"].values())
|
|
257
|
+
if usage["tokens"] + reserved >= budget["tokens"] or (
|
|
258
|
+
usage["active_seconds"] >= budget["active_seconds"]
|
|
259
|
+
):
|
|
260
|
+
self.record["blockers"] = ["Shared factory budget exhausted"]
|
|
261
|
+
self.request_pause()
|
|
262
|
+
msg = "Shared factory budget exhausted"
|
|
263
|
+
raise FactoryError(msg)
|
|
264
|
+
|
|
265
|
+
async def reserve(self, attempt: str, call: str, tokens: int) -> dict[str, Any]:
|
|
266
|
+
"""Reserve conservative capacity before allowing any provider request."""
|
|
267
|
+
async with self.lock:
|
|
268
|
+
self.ensure_attempt(attempt)
|
|
269
|
+
self.ensure_active()
|
|
270
|
+
if type(tokens) is not int or tokens <= 0 or not call:
|
|
271
|
+
msg = "Invalid model reservation"
|
|
272
|
+
raise FactoryError(msg)
|
|
273
|
+
key = attempt + ":" + call
|
|
274
|
+
if key in self.record["reservations"]:
|
|
275
|
+
msg = "Duplicate model reservation"
|
|
276
|
+
raise FactoryError(msg)
|
|
277
|
+
reserved = sum(r["tokens"] for r in self.record["reservations"].values())
|
|
278
|
+
if self.record["usage"]["tokens"] + reserved + tokens > self.record["budget"]["tokens"]:
|
|
279
|
+
self.record["blockers"] = ["Insufficient capacity for the next model call"]
|
|
280
|
+
self.request_pause()
|
|
281
|
+
raise FactoryError(self.record["blockers"][0])
|
|
282
|
+
self.record["reservations"][key] = {"tokens": tokens, "attempt": attempt}
|
|
283
|
+
self.save()
|
|
284
|
+
return {"reservation": key}
|
|
285
|
+
|
|
286
|
+
async def reconcile(self, attempt: str, key: str, actual: int | None) -> None:
|
|
287
|
+
"""Charge unknown/failed calls conservatively; never silently reclaim uncertain usage."""
|
|
288
|
+
async with self.lock:
|
|
289
|
+
worker = self.ensure_attempt(attempt)
|
|
290
|
+
reservation = self.record["reservations"].get(key)
|
|
291
|
+
if not reservation or reservation["attempt"] != attempt:
|
|
292
|
+
msg = "Foreign or already reconciled model reservation"
|
|
293
|
+
raise FactoryError(msg)
|
|
294
|
+
if actual is not None and (type(actual) is not int or actual < 0):
|
|
295
|
+
msg = "Invalid provider usage"
|
|
296
|
+
raise FactoryError(msg)
|
|
297
|
+
amount = reservation["tokens"] if actual is None else actual
|
|
298
|
+
usage = self.record["usage"]
|
|
299
|
+
usage["tokens"] += amount
|
|
300
|
+
role = usage["by_role"].setdefault(worker["role"], {"tokens": 0, "estimated_calls": 0})
|
|
301
|
+
role["tokens"] += amount
|
|
302
|
+
role["estimated_calls"] += actual is None
|
|
303
|
+
del self.record["reservations"][key]
|
|
304
|
+
self.save()
|
|
305
|
+
if usage["tokens"] >= self.record["budget"]["tokens"]:
|
|
306
|
+
self.record["blockers"] = ["Shared factory budget exhausted"]
|
|
307
|
+
self.request_pause()
|
|
308
|
+
|
|
309
|
+
def ensure_attempt(self, identity: str) -> dict[str, Any]:
|
|
310
|
+
"""Accept events only through the host-bound current attempt identity."""
|
|
311
|
+
found = next((a for a in self.record["attempts"] if a["id"] == identity), None)
|
|
312
|
+
if found is None or found["status"] not in ACTIVE:
|
|
313
|
+
msg = "Unknown, stale or completed worker attempt"
|
|
314
|
+
raise FactoryError(msg)
|
|
315
|
+
return found
|
|
316
|
+
|
|
317
|
+
def start_attempt(self, role: str, task: str, cwd: Path) -> dict[str, Any]:
|
|
318
|
+
"""Persist trusted identity before allocating a process."""
|
|
319
|
+
self.ensure_active()
|
|
320
|
+
if (
|
|
321
|
+
role not in ROLES
|
|
322
|
+
or sum(a["status"] in ACTIVE for a in self.record["attempts"]) >= MAX_SPECIALISTS
|
|
323
|
+
):
|
|
324
|
+
msg = "Factory specialist limit reached or invalid role"
|
|
325
|
+
raise FactoryError(msg)
|
|
326
|
+
if role in {"developer", "test_engineer"} and self.record["stage"] == "implementation":
|
|
327
|
+
self.require_plan()
|
|
328
|
+
elif role == "developer":
|
|
329
|
+
msg = "Implementation cannot begin before plan approval"
|
|
330
|
+
raise FactoryError(msg)
|
|
331
|
+
attempt = {
|
|
332
|
+
"id": uuid.uuid4().hex,
|
|
333
|
+
"run": self.record["id"],
|
|
334
|
+
"role": role,
|
|
335
|
+
"task": task,
|
|
336
|
+
"cwd": str(cwd.resolve()),
|
|
337
|
+
"status": "starting",
|
|
338
|
+
"route": self.record["routes"][role],
|
|
339
|
+
"recovery": None,
|
|
340
|
+
}
|
|
341
|
+
import psutil
|
|
342
|
+
|
|
343
|
+
isolation = self.record["isolation"]
|
|
344
|
+
container = isinstance(isolation, dict) and not task.startswith("command-")
|
|
345
|
+
attempt["recovery"] = {
|
|
346
|
+
"kind": "factory_start",
|
|
347
|
+
"attempt": attempt["id"],
|
|
348
|
+
"since": time.time(),
|
|
349
|
+
"owner": psutil.Process().username(),
|
|
350
|
+
"container": container,
|
|
351
|
+
"name": "nova-team-" + attempt["id"],
|
|
352
|
+
"labels": {
|
|
353
|
+
"nova.agent": attempt["id"],
|
|
354
|
+
"nova.team": self.record["id"],
|
|
355
|
+
"nova.root": self.record["id"],
|
|
356
|
+
"nova.project": self.record["root"],
|
|
357
|
+
"nova.attempt": attempt["id"],
|
|
358
|
+
},
|
|
359
|
+
}
|
|
360
|
+
self.record["attempts"].append(attempt)
|
|
361
|
+
self.save()
|
|
362
|
+
return attempt
|
|
363
|
+
|
|
364
|
+
def require_plan(self) -> None:
|
|
365
|
+
"""Freeze scope, environment, models, budgets and acceptance after human review."""
|
|
366
|
+
approval = self.record["approvals"].get("plan")
|
|
367
|
+
if approval != digest(plan_definition(self.record)):
|
|
368
|
+
msg = "The current plan requires human approval"
|
|
369
|
+
raise FactoryError(msg)
|
|
370
|
+
|
|
371
|
+
def waive(self, target: str, reason: str) -> None:
|
|
372
|
+
"""Record an explicit human exception for one check or finding on one candidate."""
|
|
373
|
+
candidate = self.record.get("candidate") or {}
|
|
374
|
+
known = {check["id"] for check in self.record["checks"]} | {
|
|
375
|
+
digest(finding) for finding in self.record.get("review_findings", [])
|
|
376
|
+
}
|
|
377
|
+
if (
|
|
378
|
+
self.record["status"] in {"cancelled", "published"}
|
|
379
|
+
or not candidate.get("id")
|
|
380
|
+
or target not in known
|
|
381
|
+
):
|
|
382
|
+
message = "Waivers require a known check or finding on the current unfinished candidate"
|
|
383
|
+
raise FactoryError(message)
|
|
384
|
+
note = bounded_text(reason, 2000)
|
|
385
|
+
self.request_pause()
|
|
386
|
+
self.record.setdefault("waivers", {})[target] = {
|
|
387
|
+
"candidate": candidate["id"],
|
|
388
|
+
"reason": note,
|
|
389
|
+
}
|
|
390
|
+
self.save()
|
|
391
|
+
|
|
392
|
+
def waived(self, target: str, candidate: str) -> bool:
|
|
393
|
+
"""A candidate change expires human exceptions without weakening acceptance definitions."""
|
|
394
|
+
return (self.record.get("waivers", {}).get(target) or {}).get("candidate") == candidate
|
|
395
|
+
|
|
396
|
+
def approve(self, kind: str, identity: str) -> None:
|
|
397
|
+
"""Called only by the owning UI response handler, never exposed as a model tool."""
|
|
398
|
+
if self.record["status"] not in {"running", "waiting_approval"}:
|
|
399
|
+
msg = "Suspended or terminal runs cannot accept an expired approval"
|
|
400
|
+
raise FactoryError(msg)
|
|
401
|
+
expected = (
|
|
402
|
+
digest(plan_definition(self.record))
|
|
403
|
+
if kind == "plan"
|
|
404
|
+
else (self.record.get("publication") or {}).get("id")
|
|
405
|
+
)
|
|
406
|
+
if kind not in {"plan", "delivery"} or not expected or expected != identity:
|
|
407
|
+
msg = "Approval does not match the current plan or publication intent"
|
|
408
|
+
raise FactoryError(msg)
|
|
409
|
+
self.record["approvals"][kind] = identity
|
|
410
|
+
self.save()
|
|
411
|
+
|
|
412
|
+
async def stop_workers(self) -> None:
|
|
413
|
+
"""Keep uncertain workers fenced until termination is proven."""
|
|
414
|
+
for key, handle in list(self.handles.items()):
|
|
415
|
+
try:
|
|
416
|
+
stopped = await handle.stop()
|
|
417
|
+
except Exception: # noqa: BLE001 - One uncertain worker cannot prevent cleanup of peers.
|
|
418
|
+
stopped = False
|
|
419
|
+
attempt = next((a for a in self.record["attempts"] if a["id"] == key), None)
|
|
420
|
+
if attempt is not None:
|
|
421
|
+
attempt["status"] = "stopped" if stopped else "uncertain"
|
|
422
|
+
if stopped:
|
|
423
|
+
self.handles.pop(key, None)
|
|
424
|
+
if not self._storage_failed:
|
|
425
|
+
self.save()
|
|
426
|
+
|
|
427
|
+
async def resume(self) -> None:
|
|
428
|
+
"""Reconcile old workers and reservations before explicitly restarting dispatch."""
|
|
429
|
+
if self.record["status"] in {"cancelled", "published"}:
|
|
430
|
+
msg = "Cancelled or published runs cannot be resumed"
|
|
431
|
+
raise FactoryError(msg)
|
|
432
|
+
from novacode_cli.teams.recovery import RecoveryHandle
|
|
433
|
+
|
|
434
|
+
for attempt in self.record["attempts"]:
|
|
435
|
+
if attempt["status"] in ACTIVE:
|
|
436
|
+
recovery = attempt.get("recovery") or {}
|
|
437
|
+
if recovery.get("kind") == "factory_start":
|
|
438
|
+
from novacode_cli.prime_factory.recovery import reconcile_start
|
|
439
|
+
|
|
440
|
+
if not await reconcile_start(recovery):
|
|
441
|
+
msg = "Worker startup remains uncertain; no work was replayed"
|
|
442
|
+
raise FactoryError(msg)
|
|
443
|
+
attempt["status"] = "stopped"
|
|
444
|
+
continue
|
|
445
|
+
handle = self.handles.get(attempt["id"]) or RecoveryHandle(
|
|
446
|
+
attempt.get("recovery") or {}
|
|
447
|
+
)
|
|
448
|
+
if not await handle.stop():
|
|
449
|
+
msg = "An uncertain worker still owns the workspace"
|
|
450
|
+
raise FactoryError(msg)
|
|
451
|
+
attempt["status"] = "stopped"
|
|
452
|
+
for key, reservation in list(self.record["reservations"].items()):
|
|
453
|
+
self.record["usage"]["tokens"] += reservation["tokens"]
|
|
454
|
+
attempt = next(
|
|
455
|
+
(a for a in self.record["attempts"] if a["id"] == reservation["attempt"]), None
|
|
456
|
+
)
|
|
457
|
+
role_name = attempt["role"] if attempt else "unknown"
|
|
458
|
+
role = self.record["usage"]["by_role"].setdefault(
|
|
459
|
+
role_name, {"tokens": 0, "estimated_calls": 0}
|
|
460
|
+
)
|
|
461
|
+
role["tokens"] += reservation["tokens"]
|
|
462
|
+
role["estimated_calls"] += 1
|
|
463
|
+
del self.record["reservations"][key]
|
|
464
|
+
self.record["approvals"].pop("delivery", None)
|
|
465
|
+
self.record["blockers"] = []
|
|
466
|
+
self.record["status"] = "running"
|
|
467
|
+
self._waiting.clear()
|
|
468
|
+
self._invalidate_approvals()
|
|
469
|
+
self._clock = time.monotonic()
|
|
470
|
+
self._active = True
|
|
471
|
+
self.save()
|
|
472
|
+
self.ensure_active()
|
|
473
|
+
|
|
474
|
+
async def wait_approval(self, kind: str, details: dict[str, Any], identity: str) -> bool:
|
|
475
|
+
"""Yield an ordinary routed plan approval, mandatory even with tool auto approval."""
|
|
476
|
+
from novacode_cli import ui_events as ev
|
|
477
|
+
|
|
478
|
+
epoch = self._approval_epoch
|
|
479
|
+
wait_key = "approval:" + kind
|
|
480
|
+
self.begin_wait(wait_key)
|
|
481
|
+
self.record["status"] = "waiting_approval"
|
|
482
|
+
self.save()
|
|
483
|
+
future = asyncio.get_running_loop().create_future()
|
|
484
|
+
self._pending_approvals.add(future)
|
|
485
|
+
await self.events.put(
|
|
486
|
+
ev.InterruptRequest(
|
|
487
|
+
"plan",
|
|
488
|
+
{
|
|
489
|
+
"plan": json.dumps(details, ensure_ascii=False, indent=2),
|
|
490
|
+
"factory": True,
|
|
491
|
+
"requires_owner_approval": True,
|
|
492
|
+
"run_id": self.record["id"],
|
|
493
|
+
"continuation_id": self.record["id"],
|
|
494
|
+
"approval_kind": kind,
|
|
495
|
+
"approval_digest": identity,
|
|
496
|
+
},
|
|
497
|
+
future,
|
|
498
|
+
)
|
|
499
|
+
)
|
|
500
|
+
try:
|
|
501
|
+
result = await future
|
|
502
|
+
finally:
|
|
503
|
+
self._pending_approvals.discard(future)
|
|
504
|
+
self.end_wait(wait_key)
|
|
505
|
+
with self.store.lock:
|
|
506
|
+
if epoch != self._approval_epoch or self.record["status"] != "waiting_approval":
|
|
507
|
+
return False
|
|
508
|
+
response = result.get("response", {}) if isinstance(result, dict) else {}
|
|
509
|
+
approved = isinstance(response, dict) and response.get("approved") is True
|
|
510
|
+
if not approved:
|
|
511
|
+
self.record["blockers"] = [
|
|
512
|
+
f"Human {kind} approval required; resume {self.record['id']}"
|
|
513
|
+
]
|
|
514
|
+
self.request_pause()
|
|
515
|
+
return False
|
|
516
|
+
self.approve(kind, identity)
|
|
517
|
+
self.record["status"] = "running"
|
|
518
|
+
self._clock = time.monotonic()
|
|
519
|
+
self._active = True
|
|
520
|
+
self.save()
|
|
521
|
+
return True
|
|
522
|
+
|
|
523
|
+
def check_result(
|
|
524
|
+
self,
|
|
525
|
+
check: dict[str, Any],
|
|
526
|
+
candidate: str,
|
|
527
|
+
environment: str,
|
|
528
|
+
argv: list[str],
|
|
529
|
+
code: int | None,
|
|
530
|
+
output: str,
|
|
531
|
+
*,
|
|
532
|
+
baseline: bool = False,
|
|
533
|
+
) -> None:
|
|
534
|
+
"""Only host-observed exact executions can satisfy frozen acceptance checks."""
|
|
535
|
+
self.require_plan()
|
|
536
|
+
if check not in self.record["checks"] or argv != check["argv"]:
|
|
537
|
+
msg = "Acceptance definition changed"
|
|
538
|
+
raise FactoryError(msg)
|
|
539
|
+
evidence = {
|
|
540
|
+
"id": uuid.uuid4().hex,
|
|
541
|
+
"check": check["id"],
|
|
542
|
+
"candidate": candidate,
|
|
543
|
+
"environment": environment,
|
|
544
|
+
"definition": digest(check_definition(check)),
|
|
545
|
+
"exit_code": code,
|
|
546
|
+
"output": safe_text(output, 12000),
|
|
547
|
+
"observed_at": time.time(),
|
|
548
|
+
"baseline": baseline,
|
|
549
|
+
}
|
|
550
|
+
check["evidence"].append(evidence)
|
|
551
|
+
if baseline:
|
|
552
|
+
check["baseline_failed"] = (
|
|
553
|
+
code is not None
|
|
554
|
+
and code not in {0, 126, 127, 9009}
|
|
555
|
+
and all(term in output for term in check.get("failure_contains", []))
|
|
556
|
+
)
|
|
557
|
+
else:
|
|
558
|
+
passed = code == 0 and all(term in output for term in check.get("contains", []))
|
|
559
|
+
unavailable = code is None or code in {126, 127, 9009}
|
|
560
|
+
check["status"] = "passed" if passed else "unavailable" if unavailable else "failed"
|
|
561
|
+
if not passed:
|
|
562
|
+
check["failures"] += 1
|
|
563
|
+
if check["test_first"] and not check.get("baseline_failed"):
|
|
564
|
+
check["status"] = "unverified"
|
|
565
|
+
self.save()
|