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,261 @@
|
|
|
1
|
+
"""Canonical event-loop entry and explicit suspended-run continuation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import re
|
|
7
|
+
import uuid
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import AsyncIterator
|
|
13
|
+
|
|
14
|
+
from novacode_cli import ui_events as ev
|
|
15
|
+
from novacode_cli.prime_factory import DEFAULT_OUTPUT_LIMIT, ROLES
|
|
16
|
+
from novacode_cli.prime_factory.controller import FactoryController, storage_path
|
|
17
|
+
from novacode_cli.prime_factory.runner import SpecialistRunner
|
|
18
|
+
from novacode_cli.prime_factory.store import FactoryError, FactoryStore
|
|
19
|
+
from novacode_cli.prime_factory.workflow import FactoryWorkflow
|
|
20
|
+
from novacode_cli.prime_memory import session_directory
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_factory_request(text: str, session: Any) -> bool: # noqa: ANN401 - Session proxies.
|
|
24
|
+
"""Only explicit development goals or selected continuations start factory work."""
|
|
25
|
+
if getattr(session, "prime_enabled", False) is not True:
|
|
26
|
+
return False
|
|
27
|
+
normalized = text.strip().lower()
|
|
28
|
+
if re.match(
|
|
29
|
+
r"^(?:(?:can you|could you|please)\s+)?"
|
|
30
|
+
r"(?:update me\b|test results\b|what\b|why\b|how\b|explain\b|"
|
|
31
|
+
r"describe\b|show\b|summarize\b|status\b)",
|
|
32
|
+
normalized,
|
|
33
|
+
):
|
|
34
|
+
return False
|
|
35
|
+
if getattr(session, "prime_factory_resume", None):
|
|
36
|
+
return True
|
|
37
|
+
if normalized.startswith("continue") and getattr(session, "prime_pending", None):
|
|
38
|
+
return True
|
|
39
|
+
return bool(
|
|
40
|
+
re.match(
|
|
41
|
+
r"^(?:(?:please|can you|could you|i want (?:you )?to)\s+)?"
|
|
42
|
+
r"(?:fix|build|implement|add|create|replace|refactor|remove|update|upgrade|develop|"
|
|
43
|
+
r"resolve|repair|complete|test|optimize|migrate|finish)\b",
|
|
44
|
+
normalized,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def model_routes(profile: Any, session: Any, agent: Any) -> dict[str, Any]: # noqa: ANN401 - Framework routes.
|
|
50
|
+
"""Inherit the actual bound session model while keeping configured roles exact."""
|
|
51
|
+
inherited = getattr(agent, "factory_model_route", None) or profile.get_model_config() or {}
|
|
52
|
+
override = getattr(session, "cli_model_override", None)
|
|
53
|
+
if isinstance(override, str) and ":" in override:
|
|
54
|
+
provider, model = override.split(":", 1)
|
|
55
|
+
inherited = {"provider": provider, "model": model}
|
|
56
|
+
routes = {role: profile.get_factory_model(role) or inherited for role in ROLES}
|
|
57
|
+
session_provider = getattr(session, "session_model_provider", None)
|
|
58
|
+
if any(not route.get("provider") or not route.get("model") for route in routes.values()):
|
|
59
|
+
message = "Factory requires explicit provider/model routes or a serializable session model"
|
|
60
|
+
raise FactoryError(message)
|
|
61
|
+
if (
|
|
62
|
+
session_provider
|
|
63
|
+
and session_provider != inherited.get("provider")
|
|
64
|
+
and any(not profile.get_factory_model(role) for role in ROLES)
|
|
65
|
+
):
|
|
66
|
+
message = "Session model route is unavailable; configure factory role models"
|
|
67
|
+
raise FactoryError(message)
|
|
68
|
+
return routes
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def prepare_controller(
|
|
72
|
+
text: str,
|
|
73
|
+
session: Any, # noqa: ANN401 - Session proxies.
|
|
74
|
+
agent: Any, # noqa: ANN401 - Compiled graph implementations.
|
|
75
|
+
store: FactoryStore,
|
|
76
|
+
root: Path,
|
|
77
|
+
*,
|
|
78
|
+
requested_thread: str | None = None,
|
|
79
|
+
) -> FactoryController:
|
|
80
|
+
"""Load a durable continuation or persist a new explicitly requested goal."""
|
|
81
|
+
requested_thread = session.thread_id if requested_thread is None else requested_thread
|
|
82
|
+
if session.thread_id != requested_thread:
|
|
83
|
+
message = "Conversation changed during factory startup; no work was dispatched"
|
|
84
|
+
raise FactoryError(message)
|
|
85
|
+
identity = getattr(session, "prime_factory_resume", None)
|
|
86
|
+
if identity:
|
|
87
|
+
record = await asyncio.to_thread(store.load, identity)
|
|
88
|
+
saved_root = await asyncio.to_thread(Path(record["root"]).resolve)
|
|
89
|
+
if saved_root != root:
|
|
90
|
+
message = "Resume from the factory's original repository workspace"
|
|
91
|
+
raise FactoryError(message)
|
|
92
|
+
record.setdefault("origin_thread", record["thread"])
|
|
93
|
+
record["thread"] = requested_thread
|
|
94
|
+
controller = FactoryController(store, record, session)
|
|
95
|
+
await controller.resume()
|
|
96
|
+
controller.ensure_active()
|
|
97
|
+
session.prime_factory_resume = None
|
|
98
|
+
if record["stage"] not in {"requirements", "research", "architecture", "plan_approval"}:
|
|
99
|
+
record["stage"] = record["phase"] = "plan_approval"
|
|
100
|
+
controller.save()
|
|
101
|
+
session.prime_factory = controller
|
|
102
|
+
return controller
|
|
103
|
+
existing = getattr(session, "prime_report", None)
|
|
104
|
+
if (
|
|
105
|
+
isinstance(existing, dict)
|
|
106
|
+
and existing.get("factory")
|
|
107
|
+
and existing.get("status")
|
|
108
|
+
not in {
|
|
109
|
+
"published",
|
|
110
|
+
"cancelled",
|
|
111
|
+
}
|
|
112
|
+
):
|
|
113
|
+
message = f"Resume or cancel existing factory run {existing['id']} first"
|
|
114
|
+
raise FactoryError(message)
|
|
115
|
+
from novacode_cli.config.nova_config import NovaConfig
|
|
116
|
+
|
|
117
|
+
profile = await asyncio.to_thread(NovaConfig)
|
|
118
|
+
routes = await asyncio.to_thread(model_routes, profile, session, agent)
|
|
119
|
+
output_limit = profile.get("max_tokens") or DEFAULT_OUTPUT_LIMIT
|
|
120
|
+
if type(output_limit) is not int or output_limit <= 0:
|
|
121
|
+
message = "Factory model output limit must be a positive integer"
|
|
122
|
+
raise FactoryError(message)
|
|
123
|
+
pending = getattr(session, "prime_pending", None)
|
|
124
|
+
goal = pending.get("goal", text) if isinstance(pending, dict) else text
|
|
125
|
+
controller = await asyncio.to_thread(
|
|
126
|
+
FactoryController.create,
|
|
127
|
+
root,
|
|
128
|
+
goal,
|
|
129
|
+
requested_thread,
|
|
130
|
+
routes,
|
|
131
|
+
session=session,
|
|
132
|
+
store=store,
|
|
133
|
+
)
|
|
134
|
+
container = profile.get("team_container")
|
|
135
|
+
controller.record["output_token_limit"] = output_limit
|
|
136
|
+
controller.save()
|
|
137
|
+
if container is not None:
|
|
138
|
+
if not isinstance(container, dict):
|
|
139
|
+
message = "Configured container profile is invalid"
|
|
140
|
+
raise FactoryError(message)
|
|
141
|
+
controller.record["isolation"] = {
|
|
142
|
+
"workers": "container",
|
|
143
|
+
"integration_checks": "worktree",
|
|
144
|
+
"container_profile": {
|
|
145
|
+
key: container.get(key)
|
|
146
|
+
for key in (
|
|
147
|
+
"image",
|
|
148
|
+
"network",
|
|
149
|
+
"user",
|
|
150
|
+
"memory",
|
|
151
|
+
"cpus",
|
|
152
|
+
"readonly",
|
|
153
|
+
)
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
controller.save()
|
|
157
|
+
controller.ensure_active()
|
|
158
|
+
session.prime_pending = None
|
|
159
|
+
session.prime_factory = controller
|
|
160
|
+
return controller
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
async def iterate_factory_events( # noqa: PLR0912, PLR0915 - Event stream and cancellation cleanup.
|
|
164
|
+
text: str,
|
|
165
|
+
session: Any, # noqa: ANN401 - Session proxies.
|
|
166
|
+
*,
|
|
167
|
+
agent: Any = None, # noqa: ANN401 - Compiled graph implementations.
|
|
168
|
+
) -> AsyncIterator[Any]:
|
|
169
|
+
"""Drive host transitions while keeping approval routing in the originating consumer."""
|
|
170
|
+
controller = None
|
|
171
|
+
driver = None
|
|
172
|
+
store = None
|
|
173
|
+
originating_thread = session.thread_id
|
|
174
|
+
try:
|
|
175
|
+
root = await asyncio.to_thread(lambda: Path(session_directory(session)).resolve())
|
|
176
|
+
store = await asyncio.to_thread(FactoryStore, storage_path(root))
|
|
177
|
+
controller = await prepare_controller(
|
|
178
|
+
text,
|
|
179
|
+
session,
|
|
180
|
+
agent,
|
|
181
|
+
store,
|
|
182
|
+
root,
|
|
183
|
+
requested_thread=originating_thread,
|
|
184
|
+
)
|
|
185
|
+
if agent is not None:
|
|
186
|
+
from langchain_core.messages import HumanMessage
|
|
187
|
+
|
|
188
|
+
await agent.aupdate_state(
|
|
189
|
+
{"configurable": {"thread_id": controller.record["thread"]}},
|
|
190
|
+
{"messages": [HumanMessage(text, id=uuid.uuid4().hex)]},
|
|
191
|
+
)
|
|
192
|
+
runner = SpecialistRunner(controller)
|
|
193
|
+
workflow = await asyncio.to_thread(FactoryWorkflow, controller, runner)
|
|
194
|
+
driver = asyncio.create_task(
|
|
195
|
+
workflow.run(), name=f"prime-factory-{controller.record['id']}"
|
|
196
|
+
)
|
|
197
|
+
while not driver.done() or not controller.events.empty():
|
|
198
|
+
try:
|
|
199
|
+
event = await asyncio.wait_for(controller.events.get(), timeout=0.25)
|
|
200
|
+
except TimeoutError:
|
|
201
|
+
continue
|
|
202
|
+
if session.thread_id != controller.record["thread"]:
|
|
203
|
+
controller.request_pause()
|
|
204
|
+
if isinstance(event, ev.InterruptRequest) and not event.future.done():
|
|
205
|
+
event.future.set_result({"response": {"approved": False}})
|
|
206
|
+
continue
|
|
207
|
+
yield event
|
|
208
|
+
await driver
|
|
209
|
+
if session.thread_id != controller.record["thread"]:
|
|
210
|
+
yield ev.Done(had_response=False)
|
|
211
|
+
return
|
|
212
|
+
report = controller.record
|
|
213
|
+
message = f"Prime factory {report['id']} · {report['status']} · {report['stage']}"
|
|
214
|
+
if report["blockers"]:
|
|
215
|
+
message += "\n" + "\n".join(report["blockers"])
|
|
216
|
+
if report["status"] not in {"published", "cancelled"}:
|
|
217
|
+
message += f"\nContinue with /prime resume {report['id']}."
|
|
218
|
+
if agent is not None:
|
|
219
|
+
from langchain_core.messages import AIMessage
|
|
220
|
+
|
|
221
|
+
await agent.aupdate_state(
|
|
222
|
+
{"configurable": {"thread_id": controller.record["thread"]}},
|
|
223
|
+
{"messages": [AIMessage(message, id=uuid.uuid4().hex)]},
|
|
224
|
+
)
|
|
225
|
+
if session.thread_id != controller.record["thread"]:
|
|
226
|
+
yield ev.Done(had_response=False)
|
|
227
|
+
return
|
|
228
|
+
yield ev.ContextMessage(
|
|
229
|
+
message,
|
|
230
|
+
event_type="nova_prime_receipt",
|
|
231
|
+
record_id=report["id"],
|
|
232
|
+
data=controller.progress(),
|
|
233
|
+
)
|
|
234
|
+
# Headless consumers need a useful continuation in their final result too.
|
|
235
|
+
yield ev.AssistantMessage(text=message, agent_name="Prime", agent_color="cyan")
|
|
236
|
+
yield ev.Done(had_response=True)
|
|
237
|
+
except asyncio.CancelledError:
|
|
238
|
+
if controller is not None:
|
|
239
|
+
controller.request_pause()
|
|
240
|
+
raise
|
|
241
|
+
except Exception as exc: # noqa: BLE001 - Frontends receive durable failure state.
|
|
242
|
+
if controller is not None and not controller._storage_failed:
|
|
243
|
+
controller.record["blockers"] = [str(exc)]
|
|
244
|
+
controller.request_pause()
|
|
245
|
+
if session.thread_id != originating_thread:
|
|
246
|
+
yield ev.Done(had_response=False)
|
|
247
|
+
return
|
|
248
|
+
yield ev.Error(f"Prime factory suspended: {exc}")
|
|
249
|
+
yield ev.Done(had_response=False)
|
|
250
|
+
finally:
|
|
251
|
+
if driver is not None and not driver.done():
|
|
252
|
+
driver.cancel()
|
|
253
|
+
await asyncio.gather(driver, return_exceptions=True)
|
|
254
|
+
if controller is not None:
|
|
255
|
+
try:
|
|
256
|
+
await controller.stop_workers()
|
|
257
|
+
finally:
|
|
258
|
+
if getattr(session, "prime_factory", None) is controller:
|
|
259
|
+
session.prime_factory = None
|
|
260
|
+
if store is not None:
|
|
261
|
+
await asyncio.to_thread(store.close)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Validated factory plans and immutable acceptance definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import PurePosixPath, PureWindowsPath
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from novacode_cli.prime_factory.store import FactoryError
|
|
11
|
+
|
|
12
|
+
MAX_OWNED_FILES = 128
|
|
13
|
+
MAX_TASKS = 64
|
|
14
|
+
MAX_CHECKS = 32
|
|
15
|
+
MAX_ARGV = 64
|
|
16
|
+
MAX_ASSERTIONS = 8
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def digest(value: object) -> str:
|
|
20
|
+
"""Hash lossless canonical data for approvals and observed evidence."""
|
|
21
|
+
return hashlib.sha256(json.dumps(value, sort_keys=True, allow_nan=False).encode()).hexdigest()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def bounded_text(value: object, limit: int = 4000) -> str:
|
|
25
|
+
"""Reject truncated or missing worker assertions."""
|
|
26
|
+
if not isinstance(value, str) or not value.strip() or len(value) > limit:
|
|
27
|
+
msg = "Expected bounded nonempty text"
|
|
28
|
+
raise FactoryError(msg)
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def paths(value: object) -> list[str]:
|
|
33
|
+
"""Validate repository-relative ownership, including Windows on Linux hosts."""
|
|
34
|
+
if not isinstance(value, list) or len(value) > MAX_OWNED_FILES:
|
|
35
|
+
msg = "Expected at most 128 owned files"
|
|
36
|
+
raise FactoryError(msg)
|
|
37
|
+
result = []
|
|
38
|
+
for item in value:
|
|
39
|
+
name = bounded_text(item, 500).replace("\\", "/")
|
|
40
|
+
path = PurePosixPath(name)
|
|
41
|
+
if (
|
|
42
|
+
path.is_absolute()
|
|
43
|
+
or PureWindowsPath(name).drive
|
|
44
|
+
or ".." in path.parts
|
|
45
|
+
or any(part in {".git", ".nova", ".env", ".aws", ".codex"} for part in path.parts)
|
|
46
|
+
or any(char in name for char in "*?[]\x00\n\r")
|
|
47
|
+
):
|
|
48
|
+
msg = "Ownership must name explicit repository files"
|
|
49
|
+
raise FactoryError(msg)
|
|
50
|
+
result.append(path.as_posix())
|
|
51
|
+
if len(result) != len(set(result)):
|
|
52
|
+
msg = "Duplicate file ownership"
|
|
53
|
+
raise FactoryError(msg)
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def validate_tasks(value: object) -> list[dict[str, Any]]:
|
|
58
|
+
"""Require an acyclic dependency graph before approving any implementation."""
|
|
59
|
+
if not isinstance(value, list) or not 1 <= len(value) <= MAX_TASKS:
|
|
60
|
+
msg = "Plan requires 1-64 tasks"
|
|
61
|
+
raise FactoryError(msg)
|
|
62
|
+
tasks: list[dict[str, Any]] = []
|
|
63
|
+
for raw in value:
|
|
64
|
+
if not isinstance(raw, dict):
|
|
65
|
+
msg = "Invalid task"
|
|
66
|
+
raise FactoryError(msg)
|
|
67
|
+
key = bounded_text(raw.get("id"), 60)
|
|
68
|
+
if not key.replace("_", "").replace("-", "").isalnum():
|
|
69
|
+
msg = "Task IDs must be simple identifiers"
|
|
70
|
+
raise FactoryError(msg)
|
|
71
|
+
dependencies = raw.get("depends_on", [])
|
|
72
|
+
if not isinstance(dependencies, list) or any(not isinstance(d, str) for d in dependencies):
|
|
73
|
+
msg = "Invalid task dependencies"
|
|
74
|
+
raise FactoryError(msg)
|
|
75
|
+
tasks.append(
|
|
76
|
+
{
|
|
77
|
+
"id": key,
|
|
78
|
+
"description": bounded_text(raw.get("description")),
|
|
79
|
+
"files": paths(raw.get("files")),
|
|
80
|
+
"depends_on": dependencies,
|
|
81
|
+
"status": "pending",
|
|
82
|
+
"attempts": [],
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
keys = {task["id"] for task in tasks}
|
|
86
|
+
if len(keys) != len(tasks):
|
|
87
|
+
msg = "Duplicate task IDs"
|
|
88
|
+
raise FactoryError(msg)
|
|
89
|
+
resolved: set[str] = set()
|
|
90
|
+
while len(resolved) < len(tasks):
|
|
91
|
+
ready = {t["id"] for t in tasks if set(t["depends_on"]) <= resolved}
|
|
92
|
+
if ready <= resolved:
|
|
93
|
+
msg = "Task dependencies contain a cycle or unknown task"
|
|
94
|
+
raise FactoryError(msg)
|
|
95
|
+
resolved |= ready
|
|
96
|
+
return tasks
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def check_definition(check: dict[str, Any]) -> dict[str, Any]:
|
|
100
|
+
"""Keep approval and execution signatures on the same acceptance fields."""
|
|
101
|
+
return {
|
|
102
|
+
**{key: check[key] for key in ("argv", "criteria", "purpose", "test_first", "security")},
|
|
103
|
+
**{key: check.get(key, []) for key in ("tasks", "contains", "failure_contains")},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def validate_checks(
|
|
108
|
+
value: object, criteria: list[str], task_ids: set[str] | list[str] | None = None
|
|
109
|
+
) -> list[dict[str, Any]]:
|
|
110
|
+
"""Assign identity from definitions so renaming cannot reset repair limits."""
|
|
111
|
+
if not isinstance(value, list) or not 1 <= len(value) <= MAX_CHECKS:
|
|
112
|
+
msg = "Define 1-32 acceptance checks"
|
|
113
|
+
raise FactoryError(msg)
|
|
114
|
+
result: list[dict[str, Any]] = []
|
|
115
|
+
for raw in value:
|
|
116
|
+
if not isinstance(raw, dict):
|
|
117
|
+
msg = "Invalid acceptance definition"
|
|
118
|
+
raise FactoryError(msg)
|
|
119
|
+
argv = raw.get("argv")
|
|
120
|
+
indexes = raw.get("criteria")
|
|
121
|
+
if not isinstance(argv, list) or not argv or len(argv) > MAX_ARGV:
|
|
122
|
+
msg = "Checks require command argument arrays"
|
|
123
|
+
raise FactoryError(msg)
|
|
124
|
+
argv = [bounded_text(arg, 2000) for arg in argv]
|
|
125
|
+
if (
|
|
126
|
+
not isinstance(indexes, list)
|
|
127
|
+
or not indexes
|
|
128
|
+
or any(type(i) is not int or not 0 <= i < len(criteria) for i in indexes)
|
|
129
|
+
):
|
|
130
|
+
msg = "Checks must cover existing acceptance criteria"
|
|
131
|
+
raise FactoryError(msg)
|
|
132
|
+
owners = raw.get("tasks", sorted(task_ids or []))
|
|
133
|
+
if (
|
|
134
|
+
not isinstance(owners, list)
|
|
135
|
+
or any(not isinstance(owner, str) for owner in owners)
|
|
136
|
+
or len(owners) != len(set(owners))
|
|
137
|
+
or (task_ids is not None and (not owners or not set(owners) <= set(task_ids)))
|
|
138
|
+
):
|
|
139
|
+
msg = "Acceptance tasks must identify responsible planned tasks"
|
|
140
|
+
raise FactoryError(msg)
|
|
141
|
+
assertions = {}
|
|
142
|
+
for field in ("contains", "failure_contains"):
|
|
143
|
+
expected = raw.get(field, [])
|
|
144
|
+
if not isinstance(expected, list) or len(expected) > MAX_ASSERTIONS:
|
|
145
|
+
msg = "Acceptance output assertions require at most eight literals"
|
|
146
|
+
raise FactoryError(msg)
|
|
147
|
+
assertions[field] = [bounded_text(term, 500) for term in expected]
|
|
148
|
+
definition = {
|
|
149
|
+
"argv": argv,
|
|
150
|
+
"criteria": sorted(set(indexes)),
|
|
151
|
+
"purpose": bounded_text(raw.get("purpose")),
|
|
152
|
+
"test_first": raw.get("test_first") is True,
|
|
153
|
+
"security": raw.get("security") is True,
|
|
154
|
+
"tasks": sorted(owners),
|
|
155
|
+
**assertions,
|
|
156
|
+
}
|
|
157
|
+
result.append(
|
|
158
|
+
{
|
|
159
|
+
**definition,
|
|
160
|
+
"id": digest(definition),
|
|
161
|
+
"failures": 0,
|
|
162
|
+
"status": "pending",
|
|
163
|
+
"evidence": [],
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
if len({c["id"] for c in result}) != len(result):
|
|
167
|
+
msg = "Duplicate acceptance definitions"
|
|
168
|
+
raise FactoryError(msg)
|
|
169
|
+
if {i for c in result for i in c["criteria"]} != set(range(len(criteria))):
|
|
170
|
+
msg = "Every criterion needs an acceptance check"
|
|
171
|
+
raise FactoryError(msg)
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def plan_definition(record: dict[str, Any]) -> dict[str, Any]:
|
|
176
|
+
"""Include every approval-relevant field while excluding mutable task/check status."""
|
|
177
|
+
return {
|
|
178
|
+
"goal": record["goal"],
|
|
179
|
+
"criteria": record["criteria"],
|
|
180
|
+
"tasks": [
|
|
181
|
+
{k: t[k] for k in ("id", "description", "files", "depends_on")} for t in record["tasks"]
|
|
182
|
+
],
|
|
183
|
+
"checks": [{"id": c["id"], **check_definition(c)} for c in record["checks"]],
|
|
184
|
+
**{
|
|
185
|
+
key: record.get(key)
|
|
186
|
+
for key in (
|
|
187
|
+
"decisions",
|
|
188
|
+
"sources",
|
|
189
|
+
"research_requirements",
|
|
190
|
+
"research_exemption",
|
|
191
|
+
"isolation",
|
|
192
|
+
"routes",
|
|
193
|
+
"budget",
|
|
194
|
+
"output_token_limit",
|
|
195
|
+
"setup",
|
|
196
|
+
"test_files",
|
|
197
|
+
"destination",
|
|
198
|
+
"baseline_selection",
|
|
199
|
+
)
|
|
200
|
+
},
|
|
201
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Atomic factory snapshots with optimistic writer fencing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sqlite3
|
|
7
|
+
from threading import RLock
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
from novacode_cli.prime_factory import DEFAULT_OUTPUT_LIMIT, VERSION
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FactoryError(ValueError):
|
|
17
|
+
"""A factory invariant requires intervention before work can continue."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def listing(record: dict[str, Any]) -> dict[str, Any]:
|
|
21
|
+
"""Project bounded display metadata separately from execution and publication artifacts."""
|
|
22
|
+
result = {
|
|
23
|
+
key: record[key]
|
|
24
|
+
for key in (
|
|
25
|
+
"id",
|
|
26
|
+
"version",
|
|
27
|
+
"factory",
|
|
28
|
+
"thread",
|
|
29
|
+
"root",
|
|
30
|
+
"goal",
|
|
31
|
+
"stage",
|
|
32
|
+
"phase",
|
|
33
|
+
"status",
|
|
34
|
+
)
|
|
35
|
+
if key in record
|
|
36
|
+
}
|
|
37
|
+
result["summary_only"] = True
|
|
38
|
+
result["checks"] = [
|
|
39
|
+
{"id": check["id"], "status": check["status"], "purpose": check.get("purpose", "")[:300]}
|
|
40
|
+
for check in record.get("checks", [])
|
|
41
|
+
]
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FactoryStore:
|
|
46
|
+
"""Commit complete snapshots before dispatching side effects."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, path: Path) -> None:
|
|
49
|
+
"""Open repository-local storage; failures propagate to the caller."""
|
|
50
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
self.lock = RLock()
|
|
52
|
+
self.db = sqlite3.connect(path, timeout=5, check_same_thread=False)
|
|
53
|
+
self.db.execute("PRAGMA journal_mode=WAL")
|
|
54
|
+
self.db.execute("PRAGMA synchronous=FULL")
|
|
55
|
+
self.db.execute(
|
|
56
|
+
"CREATE TABLE IF NOT EXISTS runs (id TEXT PRIMARY KEY, revision INTEGER, body TEXT)"
|
|
57
|
+
)
|
|
58
|
+
columns = {row[1] for row in self.db.execute("PRAGMA table_info(runs)")}
|
|
59
|
+
if "summary" not in columns:
|
|
60
|
+
self.db.execute("ALTER TABLE runs ADD COLUMN summary TEXT")
|
|
61
|
+
self.db.commit()
|
|
62
|
+
|
|
63
|
+
def save(self, record: dict[str, Any]) -> None:
|
|
64
|
+
"""Reject stale controllers rather than allowing two schedulers to dispatch."""
|
|
65
|
+
with self.lock:
|
|
66
|
+
revision = record.get("store_revision", 0)
|
|
67
|
+
snapshot = {**record, "store_revision": revision + 1}
|
|
68
|
+
encoded = json.dumps(snapshot, allow_nan=False, sort_keys=True)
|
|
69
|
+
from novacode_cli.security.prompt_injection import _credential_values
|
|
70
|
+
|
|
71
|
+
if any(json.dumps(secret)[1:-1] in encoded for secret in _credential_values()):
|
|
72
|
+
message = "Factory records cannot contain credentials; use environment references"
|
|
73
|
+
raise FactoryError(message)
|
|
74
|
+
summary = json.dumps(listing(snapshot), allow_nan=False)
|
|
75
|
+
with self.db:
|
|
76
|
+
if revision == 0:
|
|
77
|
+
self.db.execute(
|
|
78
|
+
"INSERT INTO runs (id, revision, body, summary) VALUES (?, ?, ?, ?)",
|
|
79
|
+
(record["id"], 1, encoded, summary),
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
cursor = self.db.execute(
|
|
83
|
+
"UPDATE runs SET revision=?, body=?, summary=? WHERE id=? AND revision=?",
|
|
84
|
+
(revision + 1, encoded, summary, record["id"], revision),
|
|
85
|
+
)
|
|
86
|
+
if cursor.rowcount != 1:
|
|
87
|
+
msg = "Factory ownership changed; reload before continuing"
|
|
88
|
+
raise FactoryError(msg)
|
|
89
|
+
record["store_revision"] = revision + 1
|
|
90
|
+
|
|
91
|
+
def load(self, identity: str) -> dict[str, Any]:
|
|
92
|
+
"""Read only this repository's versioned factory records."""
|
|
93
|
+
with self.lock:
|
|
94
|
+
row = self.db.execute("SELECT body FROM runs WHERE id=?", (identity,)).fetchone()
|
|
95
|
+
if row is None:
|
|
96
|
+
msg = "Factory run not found in this repository"
|
|
97
|
+
raise FactoryError(msg)
|
|
98
|
+
record = json.loads(row[0])
|
|
99
|
+
if record.get("version") != VERSION or record.get("factory") is not True:
|
|
100
|
+
msg = "Unsupported factory record version"
|
|
101
|
+
raise FactoryError(msg)
|
|
102
|
+
record.setdefault("output_token_limit", DEFAULT_OUTPUT_LIMIT)
|
|
103
|
+
return record
|
|
104
|
+
|
|
105
|
+
def records(self) -> list[dict[str, Any]]:
|
|
106
|
+
"""Return bounded recent summaries without loading command output."""
|
|
107
|
+
with self.lock:
|
|
108
|
+
rows = self.db.execute(
|
|
109
|
+
"SELECT id, summary FROM runs ORDER BY rowid DESC LIMIT 50"
|
|
110
|
+
).fetchall()
|
|
111
|
+
result = []
|
|
112
|
+
for identity, stored_summary in rows:
|
|
113
|
+
summary = stored_summary
|
|
114
|
+
if summary is None:
|
|
115
|
+
# Upgrade one old record at a time, never materialize fifty full histories.
|
|
116
|
+
record = self.load(identity)
|
|
117
|
+
summary = json.dumps(listing(record))
|
|
118
|
+
with self.db:
|
|
119
|
+
self.db.execute(
|
|
120
|
+
"UPDATE runs SET summary=? WHERE id=? AND revision=?",
|
|
121
|
+
(summary, identity, record["store_revision"]),
|
|
122
|
+
)
|
|
123
|
+
result.append(json.loads(summary))
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
def close(self) -> None:
|
|
127
|
+
"""Release the database after all owned workers have stopped."""
|
|
128
|
+
self.db.close()
|