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,335 @@
|
|
|
1
|
+
"""Skill management — create, clean up, and refine skills from review feedback.
|
|
2
|
+
|
|
3
|
+
Extracted from ``NovaLearningMiddleware``. Delegates to ``skill_discovery``
|
|
4
|
+
for the actual pattern-detection logic; this module owns the orchestration
|
|
5
|
+
decisions (when to create, guard against duplicates, rate-limit refinement).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from langgraph.store.base import BaseStore
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("nova.hermes.skill_manager")
|
|
21
|
+
|
|
22
|
+
# Minimum wall-clock gap between curation passes (curation is piggybacked on the
|
|
23
|
+
# review cycle but gated to roughly daily so cadence doesn't depend on chattiness).
|
|
24
|
+
_CURATION_INTERVAL_SECS = 86_400.0 # 24h
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Skill proposals seen so far, by project (see SkillManager._proposed_elsewhere).
|
|
28
|
+
_PROPOSALS_FILE = ".skill-proposals.json"
|
|
29
|
+
|
|
30
|
+
# `<scope>general</scope>` inside a review's `<skill>` block.
|
|
31
|
+
_GENERAL_SKILL_RE = re.compile(
|
|
32
|
+
r"<skill>.*?<scope>\s*general\s*</scope>.*?</skill>", re.DOTALL | re.IGNORECASE
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _similar_skill(skills_dir: Path, name: str, overlap: float = 0.6) -> str | None:
|
|
37
|
+
"""An existing skill whose name is mostly the same words as ``name``, if any.
|
|
38
|
+
|
|
39
|
+
An exact match is not reported: writing over a skill of the same name is the
|
|
40
|
+
existing update path and stays as it was.
|
|
41
|
+
"""
|
|
42
|
+
words = {w for w in re.split(r"[-_\s]+", name.lower()) if w}
|
|
43
|
+
if not words or not skills_dir.is_dir():
|
|
44
|
+
return None
|
|
45
|
+
for path in skills_dir.iterdir():
|
|
46
|
+
if not path.is_dir() or path.name == name:
|
|
47
|
+
continue
|
|
48
|
+
other = {w for w in re.split(r"[-_\s]+", path.name.lower()) if w}
|
|
49
|
+
if other and len(words & other) / len(words | other) >= overlap:
|
|
50
|
+
return path.name
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SkillManager:
|
|
55
|
+
"""Manages skill lifecycle: creation from reviews, legacy cleanup, refinement.
|
|
56
|
+
|
|
57
|
+
Used by ``ReviewRunner`` (via the middleware) after a review cycle completes.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
store: BaseStore,
|
|
63
|
+
*,
|
|
64
|
+
skills_dir: Path | None = None,
|
|
65
|
+
enabled: bool = True,
|
|
66
|
+
project: str | None = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
self._store = store
|
|
69
|
+
self._project = project or ""
|
|
70
|
+
self._skills_dir = skills_dir
|
|
71
|
+
self._enabled = enabled
|
|
72
|
+
self._refinement_tasks: set[asyncio.Task] = set()
|
|
73
|
+
|
|
74
|
+
# -- Background task management -----------------------------------------
|
|
75
|
+
|
|
76
|
+
def spawn_task(self, coro) -> None:
|
|
77
|
+
"""Run a skill create/refine coroutine as a tracked background task."""
|
|
78
|
+
task = asyncio.create_task(coro)
|
|
79
|
+
self._refinement_tasks.add(task)
|
|
80
|
+
|
|
81
|
+
def _done(t: asyncio.Task) -> None:
|
|
82
|
+
self._refinement_tasks.discard(t)
|
|
83
|
+
if not t.cancelled() and t.exception():
|
|
84
|
+
logger.error("Skill task failed: %s", t.exception())
|
|
85
|
+
|
|
86
|
+
task.add_done_callback(_done)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def pending_tasks(self) -> set[asyncio.Task]:
|
|
90
|
+
"""Return the set of tracked background tasks."""
|
|
91
|
+
return self._refinement_tasks
|
|
92
|
+
|
|
93
|
+
def _log_refinement(self, action: str, target: str, detail: str = "") -> None:
|
|
94
|
+
"""Append a skill refinement event to the unified audit trail.
|
|
95
|
+
|
|
96
|
+
Best-effort: a failed append never blocks the skill mutation. The skills
|
|
97
|
+
dir lives at ``~/.nova/skills``, so the shared ``~/.nova`` root is its
|
|
98
|
+
parent.
|
|
99
|
+
"""
|
|
100
|
+
if not self._skills_dir:
|
|
101
|
+
return
|
|
102
|
+
try:
|
|
103
|
+
from novacode_cli.hermes.refinement_log import append_refinement_event
|
|
104
|
+
|
|
105
|
+
append_refinement_event(
|
|
106
|
+
self._skills_dir.parent,
|
|
107
|
+
domain="skill",
|
|
108
|
+
action=action,
|
|
109
|
+
target=target,
|
|
110
|
+
detail=detail,
|
|
111
|
+
)
|
|
112
|
+
except Exception: # noqa: BLE001
|
|
113
|
+
logger.debug("Could not append skill refinement event", exc_info=True)
|
|
114
|
+
|
|
115
|
+
# -- Skill creation from review -----------------------------------------
|
|
116
|
+
|
|
117
|
+
async def maybe_create_from_review(self, review_text: str) -> None:
|
|
118
|
+
"""Persist an episode-grounded skill if the review proposed one.
|
|
119
|
+
|
|
120
|
+
The review LLM writes a ``<skill>`` block (name + trigger + steps) only
|
|
121
|
+
when it recognizes a genuinely reusable workflow from this session.
|
|
122
|
+
Deduped by skill name (directory existence + store record).
|
|
123
|
+
"""
|
|
124
|
+
if not self._enabled or not self._skills_dir:
|
|
125
|
+
return
|
|
126
|
+
try:
|
|
127
|
+
from novacode_cli.hermes.skill_discovery import (
|
|
128
|
+
parse_skill_spec,
|
|
129
|
+
write_skill_from_spec,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
spec = parse_skill_spec(review_text)
|
|
133
|
+
if spec is None:
|
|
134
|
+
return
|
|
135
|
+
# Skills live in one directory shared by every project. A workflow
|
|
136
|
+
# that only fits a throwaway sandbox would be offered everywhere
|
|
137
|
+
# afterwards and useful nowhere: on Terminal-Bench the loop saved
|
|
138
|
+
# `build-rust-cpp-polyglot` — one task's answer — as a skill. Keep
|
|
139
|
+
# only what the review marked as general.
|
|
140
|
+
if self._project.startswith("sandbox-"):
|
|
141
|
+
if not _GENERAL_SKILL_RE.search(review_text):
|
|
142
|
+
self._log_refinement("skill", "skip", f"{spec.get('name')} (sandbox-specific)")
|
|
143
|
+
return
|
|
144
|
+
# "General" is the review's opinion, and it is generous with it
|
|
145
|
+
# (it called `write-arithmetic-coder-encoder` general). Wait for
|
|
146
|
+
# a second, different sandbox to propose the same kind of skill.
|
|
147
|
+
if not self._proposed_elsewhere(spec):
|
|
148
|
+
self._log_refinement("skill", "hold", f"{spec.get('name')} (seen in one project)")
|
|
149
|
+
return
|
|
150
|
+
# Reviews name skills freely, so one workflow came back as
|
|
151
|
+
# `setup-python-torch-container` and `setup-python-torch-env`.
|
|
152
|
+
# Exact-name dedup does not catch that; word overlap does.
|
|
153
|
+
twin = _similar_skill(self._skills_dir, spec.get("name") or "")
|
|
154
|
+
if twin:
|
|
155
|
+
self._log_refinement("skill", "skip", f"{spec.get('name')} (same as {twin})")
|
|
156
|
+
return
|
|
157
|
+
await write_skill_from_spec(spec, self._skills_dir, self._store)
|
|
158
|
+
self._log_refinement("skill", "create", spec.get("name") or "unknown")
|
|
159
|
+
except Exception: # noqa: BLE001
|
|
160
|
+
logger.exception("Failed to create skill from review")
|
|
161
|
+
|
|
162
|
+
def _proposed_elsewhere(self, spec: dict) -> bool:
|
|
163
|
+
"""Has a different project already proposed this kind of skill?
|
|
164
|
+
|
|
165
|
+
Proposals are remembered in a small ledger beside the skills. A match is
|
|
166
|
+
a similar name or a description stating the same thing; the first
|
|
167
|
+
sighting is recorded and answers False.
|
|
168
|
+
"""
|
|
169
|
+
from novacode_cli.hermes.memory_tiers import _fact_words, _same_fact
|
|
170
|
+
|
|
171
|
+
name = str(spec.get("name") or "")
|
|
172
|
+
described = _fact_words(str(spec.get("description") or ""))
|
|
173
|
+
named = {w for w in re.split(r"[-_\s]+", name.lower()) if w}
|
|
174
|
+
ledger_path = self._skills_dir / _PROPOSALS_FILE
|
|
175
|
+
try:
|
|
176
|
+
ledger = json.loads(ledger_path.read_text(encoding="utf-8"))
|
|
177
|
+
except (OSError, ValueError):
|
|
178
|
+
ledger = {}
|
|
179
|
+
for other_name, entry in ledger.items():
|
|
180
|
+
if entry.get("project") == self._project:
|
|
181
|
+
continue
|
|
182
|
+
other_named = {w for w in re.split(r"[-_\s]+", other_name.lower()) if w}
|
|
183
|
+
same_name = named and len(named & other_named) / len(named | other_named) >= 0.6
|
|
184
|
+
if same_name or _same_fact(described, frozenset(entry.get("words") or ())):
|
|
185
|
+
return True
|
|
186
|
+
# One entry per idea per project: a long task re-proposes the same skill
|
|
187
|
+
# under a new name at every review (five "raw tensor dump" variants from
|
|
188
|
+
# one task), and none of those is a second opinion.
|
|
189
|
+
for other_name, entry in ledger.items():
|
|
190
|
+
if entry.get("project") != self._project:
|
|
191
|
+
continue
|
|
192
|
+
other_named = {w for w in re.split(r"[-_\s]+", other_name.lower()) if w}
|
|
193
|
+
# Looser than the cross-project test (share of the SHORTER name):
|
|
194
|
+
# `inspect-raw-tensor-dump` and `reverse-engineer-raw-tensor-dump`
|
|
195
|
+
# from one task are plainly the same proposal.
|
|
196
|
+
same_name = (
|
|
197
|
+
named
|
|
198
|
+
and other_named
|
|
199
|
+
and len(named & other_named) / min(len(named), len(other_named)) >= 0.6
|
|
200
|
+
)
|
|
201
|
+
if same_name or _same_fact(described, frozenset(entry.get("words") or ())):
|
|
202
|
+
return False # already holding this one
|
|
203
|
+
ledger[name] = {"project": self._project, "words": sorted(described)}
|
|
204
|
+
try:
|
|
205
|
+
self._skills_dir.mkdir(parents=True, exist_ok=True)
|
|
206
|
+
ledger_path.write_text(json.dumps(ledger, indent=1), encoding="utf-8")
|
|
207
|
+
except OSError:
|
|
208
|
+
logger.debug("Could not record skill proposal", exc_info=True)
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
# -- Legacy cleanup -----------------------------------------------------
|
|
212
|
+
|
|
213
|
+
async def cleanup_legacy_skills_once(self) -> None:
|
|
214
|
+
"""One-time removal of old ``nova-<hash>`` n-gram skills (guarded)."""
|
|
215
|
+
if not self._enabled or not self._skills_dir:
|
|
216
|
+
return
|
|
217
|
+
try:
|
|
218
|
+
done = await self._store.aget(("nova", "meta"), "legacy_skills_cleaned_v1")
|
|
219
|
+
if done is not None:
|
|
220
|
+
return
|
|
221
|
+
from novacode_cli.hermes.skill_discovery import (
|
|
222
|
+
cleanup_legacy_pattern_skills,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
removed = cleanup_legacy_pattern_skills(self._skills_dir)
|
|
226
|
+
await self._store.aput(
|
|
227
|
+
("nova", "meta"), "legacy_skills_cleaned_v1", {"removed": removed}
|
|
228
|
+
)
|
|
229
|
+
if removed:
|
|
230
|
+
from novacode_cli.events import nova_event_log
|
|
231
|
+
|
|
232
|
+
nova_event_log.append(
|
|
233
|
+
(
|
|
234
|
+
"nova_skill_refinement",
|
|
235
|
+
"◐",
|
|
236
|
+
"yellow",
|
|
237
|
+
f"▤ Nova: removed {len(removed)} legacy auto-skill(s) "
|
|
238
|
+
"(opaque nova-* patterns; replaced by episode-grounded skills)",
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
except Exception: # noqa: BLE001
|
|
242
|
+
logger.exception("Failed legacy skill cleanup")
|
|
243
|
+
|
|
244
|
+
# -- Skill refinement ---------------------------------------------------
|
|
245
|
+
|
|
246
|
+
async def maybe_refine_skills(self) -> None:
|
|
247
|
+
"""Refine one high-failure skill per cycle, grounded in its failures.
|
|
248
|
+
|
|
249
|
+
``check_skill_effectiveness`` only surfaces ``high_failure`` candidates
|
|
250
|
+
(with a cooldown + attempt cap baked in), so a skill can't be re-refined
|
|
251
|
+
on every cycle. Chronically-unused skills are handled by the curator
|
|
252
|
+
(archival), not by rewriting — see ``check_skill_effectiveness``.
|
|
253
|
+
"""
|
|
254
|
+
if not self._enabled or not self._skills_dir:
|
|
255
|
+
return
|
|
256
|
+
try:
|
|
257
|
+
from novacode_cli.hermes.skill_discovery import (
|
|
258
|
+
check_skill_effectiveness,
|
|
259
|
+
refine_skill,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
candidates = await check_skill_effectiveness(self._store)
|
|
263
|
+
for skill_name, issue in candidates:
|
|
264
|
+
if issue != "high_failure":
|
|
265
|
+
continue
|
|
266
|
+
if not (self._skills_dir / skill_name / "SKILL.md").exists():
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
# Pull captured failure samples so the refinement is grounded in
|
|
270
|
+
# what actually went wrong, not a blind regenerate.
|
|
271
|
+
failure_samples: list = []
|
|
272
|
+
try:
|
|
273
|
+
entry = await self._store.aget(("nova", "skill_usage"), skill_name)
|
|
274
|
+
if entry and isinstance(entry.value, dict):
|
|
275
|
+
failure_samples = list(entry.value.get("failure_samples") or [])
|
|
276
|
+
except Exception: # noqa: BLE001
|
|
277
|
+
failure_samples = []
|
|
278
|
+
|
|
279
|
+
from novacode_cli.events import nova_event_log
|
|
280
|
+
|
|
281
|
+
nova_event_log.append(
|
|
282
|
+
(
|
|
283
|
+
"nova_skill_refinement",
|
|
284
|
+
"◐",
|
|
285
|
+
"yellow",
|
|
286
|
+
f"◐ Nova: skill '{skill_name}' needs refinement ({issue})",
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
self.spawn_task(
|
|
290
|
+
refine_skill(
|
|
291
|
+
skill_name,
|
|
292
|
+
self._skills_dir,
|
|
293
|
+
issue,
|
|
294
|
+
failure_samples=failure_samples,
|
|
295
|
+
store=self._store,
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
self._log_refinement("refine", skill_name, f"high_failure: {issue}")
|
|
299
|
+
break # one refinement per cycle
|
|
300
|
+
except Exception: # noqa: BLE001
|
|
301
|
+
logger.exception("Failed to check skill effectiveness / refine")
|
|
302
|
+
|
|
303
|
+
# -- Skill curation -----------------------------------------------------
|
|
304
|
+
|
|
305
|
+
async def maybe_curate(self) -> None:
|
|
306
|
+
"""Run a background curation pass on a real schedule (≥ once/24h).
|
|
307
|
+
|
|
308
|
+
Called opportunistically (every Nth review), but a time gate makes the
|
|
309
|
+
cadence wall-clock-based, not review-count-based — so curation runs about
|
|
310
|
+
daily regardless of how chatty a session is.
|
|
311
|
+
"""
|
|
312
|
+
if not self._enabled or not self._skills_dir:
|
|
313
|
+
return
|
|
314
|
+
try:
|
|
315
|
+
import time
|
|
316
|
+
|
|
317
|
+
now = time.time()
|
|
318
|
+
last = 0.0
|
|
319
|
+
try:
|
|
320
|
+
rec = await self._store.aget(("nova", "meta"), "last_curation_ts")
|
|
321
|
+
if rec and isinstance(rec.value, dict):
|
|
322
|
+
last = float(rec.value.get("ts", 0.0))
|
|
323
|
+
except Exception: # noqa: BLE001
|
|
324
|
+
last = 0.0
|
|
325
|
+
if now - last < _CURATION_INTERVAL_SECS:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
# Stamp before spawning so a slow pass can't trigger a duplicate.
|
|
329
|
+
await self._store.aput(("nova", "meta"), "last_curation_ts", {"ts": now})
|
|
330
|
+
|
|
331
|
+
from novacode_cli.hermes.curator import run_curation
|
|
332
|
+
|
|
333
|
+
self.spawn_task(run_curation(self._store, self._skills_dir))
|
|
334
|
+
except Exception: # noqa: BLE001
|
|
335
|
+
logger.exception("Failed to start skill curation")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Stage autonomous skill changes without changing the active library."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from novacode_cli.hermes.activation import content_hash
|
|
8
|
+
from novacode_cli.hermes.ledger import auto_approve_enabled, main_agent_learning_ledger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def propose_skill(
|
|
12
|
+
name: str,
|
|
13
|
+
content: str,
|
|
14
|
+
*,
|
|
15
|
+
source: Path | None = None,
|
|
16
|
+
archive: bool = False,
|
|
17
|
+
origin: str = "skill_manage",
|
|
18
|
+
evidence: tuple[str, ...] = (),
|
|
19
|
+
) -> str:
|
|
20
|
+
"""Return a revision reference for a create, update, or recoverable archive."""
|
|
21
|
+
from novacode_cli.config.config import Settings
|
|
22
|
+
from novacode_cli.hermes.memory_tiers import project_memory_key
|
|
23
|
+
from novacode_cli.skills.schema import prepare_skill_content
|
|
24
|
+
|
|
25
|
+
if not archive:
|
|
26
|
+
content = prepare_skill_content(content, name)
|
|
27
|
+
settings = Settings.from_environment()
|
|
28
|
+
user_root = settings.ensure_user_skills_dir().resolve()
|
|
29
|
+
scope = "shared" if source is None or source.resolve().parent.parent == user_root else "project"
|
|
30
|
+
source_hash = content_hash(source.read_bytes()) if source else ""
|
|
31
|
+
record = main_agent_learning_ledger().propose(
|
|
32
|
+
content=content,
|
|
33
|
+
project=project_memory_key(Path.cwd()),
|
|
34
|
+
scope=scope,
|
|
35
|
+
kind="skill_archive" if archive else "skill",
|
|
36
|
+
target=name,
|
|
37
|
+
source_hash=source_hash,
|
|
38
|
+
source_path=str(source.resolve()) if source else "",
|
|
39
|
+
origin=origin,
|
|
40
|
+
evidence=evidence,
|
|
41
|
+
auto_approve=auto_approve_enabled(),
|
|
42
|
+
)
|
|
43
|
+
return record.reference
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Deterministic test-gaming detector — Loop-Engineering "Verification that Learns".
|
|
2
|
+
|
|
3
|
+
The LLM rubric grader (``nova_verify.jinja``) is the final judge of whether an
|
|
4
|
+
agent weakened its tests to make them pass. This module gives that grader
|
|
5
|
+
*concrete signals* by scanning the agent's diffs for edits to test files that
|
|
6
|
+
are classic reward-hacking patterns:
|
|
7
|
+
|
|
8
|
+
- removed or weakened ``assert`` statements,
|
|
9
|
+
- added ``@pytest.mark.skip`` / ``skipif`` / ``xfail``,
|
|
10
|
+
- changed expected values to match the (possibly broken) output,
|
|
11
|
+
- deleted failing test cases,
|
|
12
|
+
- added trivial always-green tests.
|
|
13
|
+
|
|
14
|
+
The detector is deliberately *conservative*: it only flags candidate lines and
|
|
15
|
+
lets the LLM make the call. It never fails a turn on its own.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
|
|
23
|
+
#: Paths that look like test files.
|
|
24
|
+
_TEST_FILE_RE = re.compile(r"(^|[/\\])(test_|tests?[/\\]|.*_test\.)", re.IGNORECASE)
|
|
25
|
+
|
|
26
|
+
#: A diff line that *removes* an assertion (leading ``-``).
|
|
27
|
+
_REMOVED_ASSERT_RE = re.compile(r"^-\s*(assert|self\.assert|expect\(|\.to(Be|Equal|Throw))")
|
|
28
|
+
#: A diff line that *adds* a skip/xfail marker.
|
|
29
|
+
_ADDED_SKIP_RE = re.compile(
|
|
30
|
+
r"^\+.*@(pytest\.mark\.(skip|skipif|xfail)|unittest\.skip|@Skip|@Disabled)"
|
|
31
|
+
)
|
|
32
|
+
#: A diff line that *removes* an assertion carrying an expected value — the
|
|
33
|
+
#: "old expected value deleted" signal (e.g. ``- assert add(2,2) == 4``). We
|
|
34
|
+
#: only match removed (``-``) lines: *adding* an assertion is legitimate.
|
|
35
|
+
_CHANGED_EXPECTED_RE = re.compile(
|
|
36
|
+
r"^-\s*(assert.*(==|!=|in\b|not in\b)|assertEqual|assert.*expected|"
|
|
37
|
+
r"expect\(.*\.to(Be|Equal|Throw)|toEqual|toBe)",
|
|
38
|
+
re.IGNORECASE,
|
|
39
|
+
)
|
|
40
|
+
#: A diff line that removes a test function entirely.
|
|
41
|
+
_REMOVED_TEST_DEF_RE = re.compile(r"^-\s*(def test_|async def test_|@pytest\.mark\.parametrize)")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class TestGamingFinding:
|
|
46
|
+
"""One candidate reward-hacking signal found in a diff."""
|
|
47
|
+
|
|
48
|
+
path: str
|
|
49
|
+
kind: str
|
|
50
|
+
line: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class TestGamingReport:
|
|
55
|
+
"""Aggregated candidate signals across all changed files."""
|
|
56
|
+
|
|
57
|
+
findings: list[TestGamingFinding] = field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def has_findings(self) -> bool:
|
|
61
|
+
"""Whether any candidate test-weakening edits were found."""
|
|
62
|
+
return bool(self.findings)
|
|
63
|
+
|
|
64
|
+
def summarize(self, max_lines: int = 12) -> str:
|
|
65
|
+
"""Render the findings as short evidence lines for the rubric prompt."""
|
|
66
|
+
if not self.findings:
|
|
67
|
+
return ""
|
|
68
|
+
lines = [f"Detected {len(self.findings)} candidate test-weakening edit(s):"]
|
|
69
|
+
lines.extend(
|
|
70
|
+
f"- {f.path}: {f.kind}: {f.line.strip()[:120]}" for f in self.findings[:max_lines]
|
|
71
|
+
)
|
|
72
|
+
if len(self.findings) > max_lines:
|
|
73
|
+
lines.append(f"... and {len(self.findings) - max_lines} more")
|
|
74
|
+
return "\n".join(lines)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _is_test_file(path: str) -> bool:
|
|
78
|
+
return bool(_TEST_FILE_RE.search(path))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def detect_test_gaming(diffs: list[str]) -> TestGamingReport:
|
|
82
|
+
"""Scan unified diffs for candidate test-weakening edits.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
diffs: Unified diff strings (as produced by
|
|
86
|
+
:func:`novacode_cli.file_ops.compute_unified_diff`).
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
A :class:`TestGamingReport` of candidate findings. Empty when no test
|
|
90
|
+
files were changed or no suspicious edits were found.
|
|
91
|
+
"""
|
|
92
|
+
report = TestGamingReport()
|
|
93
|
+
for diff in diffs or []:
|
|
94
|
+
path = _diff_path(diff)
|
|
95
|
+
if not path or not _is_test_file(path):
|
|
96
|
+
continue
|
|
97
|
+
for line in diff.splitlines():
|
|
98
|
+
if _REMOVED_ASSERT_RE.match(line):
|
|
99
|
+
report.findings.append(
|
|
100
|
+
TestGamingFinding(path=path, kind="removed-assert", line=line)
|
|
101
|
+
)
|
|
102
|
+
elif _ADDED_SKIP_RE.match(line):
|
|
103
|
+
report.findings.append(TestGamingFinding(path=path, kind="added-skip", line=line))
|
|
104
|
+
elif _REMOVED_TEST_DEF_RE.match(line):
|
|
105
|
+
report.findings.append(TestGamingFinding(path=path, kind="removed-test", line=line))
|
|
106
|
+
elif _CHANGED_EXPECTED_RE.match(line):
|
|
107
|
+
report.findings.append(
|
|
108
|
+
TestGamingFinding(path=path, kind="changed-expected", line=line)
|
|
109
|
+
)
|
|
110
|
+
return report
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _diff_path(diff: str) -> str | None:
|
|
114
|
+
"""Extract the file path from a unified diff header (``+++ b/path``)."""
|
|
115
|
+
for line in diff.splitlines():
|
|
116
|
+
if line.startswith("+++ "):
|
|
117
|
+
# Strip the "a/"/"b/" prefix and any "(before)"/"(after)" suffix.
|
|
118
|
+
return line[4:].split(" (")[0].lstrip("ab/")
|
|
119
|
+
return None
|