devcouncil 0.3.1 → 0.4.1
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.
- package/README.md +92 -33
- package/package.json +6 -2
- package/packages/codeintel-grammars/hatch_build.py +43 -0
- package/packages/codeintel-grammars/pyproject.toml +16 -0
- package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
- package/pyproject.toml +99 -4
- package/src/devcouncil/app/config.py +512 -20
- package/src/devcouncil/app/events.py +4 -23
- package/src/devcouncil/app/orchestrator.py +5 -0
- package/src/devcouncil/app/run_context.py +3 -3
- package/src/devcouncil/assets/__init__.py +4 -1
- package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
- package/src/devcouncil/campaign/__init__.py +71 -0
- package/src/devcouncil/campaign/bloom.py +137 -0
- package/src/devcouncil/campaign/dashboard.py +123 -0
- package/src/devcouncil/campaign/mailbox.py +305 -0
- package/src/devcouncil/campaign/notify.py +91 -0
- package/src/devcouncil/campaign/orchestrator.py +592 -0
- package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
- package/src/devcouncil/campaign/prompts/director.md +21 -0
- package/src/devcouncil/campaign/prompts/protocol.md +46 -0
- package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
- package/src/devcouncil/campaign/prompts/worker.md +24 -0
- package/src/devcouncil/campaign/roles.py +202 -0
- package/src/devcouncil/campaign/watcher.py +153 -0
- package/src/devcouncil/cli/commands/agents.py +24 -17
- package/src/devcouncil/cli/commands/artifacts.py +36 -27
- package/src/devcouncil/cli/commands/ast.py +12 -3
- package/src/devcouncil/cli/commands/baseline.py +21 -12
- package/src/devcouncil/cli/commands/boot.py +218 -0
- package/src/devcouncil/cli/commands/campaign.py +302 -0
- package/src/devcouncil/cli/commands/check.py +225 -12
- package/src/devcouncil/cli/commands/config.py +221 -74
- package/src/devcouncil/cli/commands/cost.py +137 -28
- package/src/devcouncil/cli/commands/dashboard.py +12 -4
- package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
- package/src/devcouncil/cli/commands/design.py +27 -17
- package/src/devcouncil/cli/commands/doctor.py +790 -8
- package/src/devcouncil/cli/commands/evidence.py +41 -20
- package/src/devcouncil/cli/commands/export.py +73 -0
- package/src/devcouncil/cli/commands/gaps.py +175 -0
- package/src/devcouncil/cli/commands/gated_write.py +76 -0
- package/src/devcouncil/cli/commands/go.py +220 -68
- package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
- package/src/devcouncil/cli/commands/handoff.py +45 -34
- package/src/devcouncil/cli/commands/hook.py +630 -85
- package/src/devcouncil/cli/commands/init.py +89 -30
- package/src/devcouncil/cli/commands/integrate.py +296 -1385
- package/src/devcouncil/cli/commands/lease.py +120 -0
- package/src/devcouncil/cli/commands/logs.py +12 -5
- package/src/devcouncil/cli/commands/lsp.py +40 -5
- package/src/devcouncil/cli/commands/map.py +317 -74
- package/src/devcouncil/cli/commands/mcp_server.py +12 -2
- package/src/devcouncil/cli/commands/okf.py +44 -6
- package/src/devcouncil/cli/commands/plan.py +184 -69
- package/src/devcouncil/cli/commands/prompt.py +26 -17
- package/src/devcouncil/cli/commands/provenance.py +79 -0
- package/src/devcouncil/cli/commands/repair.py +60 -49
- package/src/devcouncil/cli/commands/report.py +148 -40
- package/src/devcouncil/cli/commands/requirements.py +104 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
- package/src/devcouncil/cli/commands/rollback.py +46 -35
- package/src/devcouncil/cli/commands/run.py +173 -8
- package/src/devcouncil/cli/commands/runs.py +298 -68
- package/src/devcouncil/cli/commands/scaffold.py +33 -12
- package/src/devcouncil/cli/commands/semantic.py +29 -14
- package/src/devcouncil/cli/commands/setup.py +103 -93
- package/src/devcouncil/cli/commands/shell.py +51 -42
- package/src/devcouncil/cli/commands/show.py +56 -42
- package/src/devcouncil/cli/commands/skills.py +29 -20
- package/src/devcouncil/cli/commands/status.py +80 -67
- package/src/devcouncil/cli/commands/task_gate.py +295 -0
- package/src/devcouncil/cli/commands/tasks.py +248 -19
- package/src/devcouncil/cli/commands/trace.py +14 -8
- package/src/devcouncil/cli/commands/verify.py +33 -8
- package/src/devcouncil/cli/commands/version.py +14 -6
- package/src/devcouncil/cli/commands/watch.py +56 -40
- package/src/devcouncil/cli/commands/watch_fs.py +30 -19
- package/src/devcouncil/cli/commands/wiki.py +278 -0
- package/src/devcouncil/cli/main.py +58 -1
- package/src/devcouncil/codeintel/__init__.py +16 -0
- package/src/devcouncil/codeintel/build_control.py +429 -0
- package/src/devcouncil/codeintel/build_worker.py +78 -0
- package/src/devcouncil/codeintel/debug/__init__.py +17 -0
- package/src/devcouncil/codeintel/debug/broker.py +114 -0
- package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
- package/src/devcouncil/codeintel/debug/consent.py +36 -0
- package/src/devcouncil/codeintel/debug/discovery.py +132 -0
- package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
- package/src/devcouncil/codeintel/debug/protocol.py +259 -0
- package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
- package/src/devcouncil/codeintel/debug/session.py +238 -0
- package/src/devcouncil/codeintel/debug/tracing.py +201 -0
- package/src/devcouncil/codeintel/languages/__init__.py +17 -0
- package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
- package/src/devcouncil/codeintel/languages/registry.py +149 -0
- package/src/devcouncil/codeintel/languages/workers.py +245 -0
- package/src/devcouncil/codeintel/query/__init__.py +5 -0
- package/src/devcouncil/codeintel/query/engine.py +289 -0
- package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
- package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
- package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
- package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
- package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
- package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
- package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
- package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
- package/src/devcouncil/codeintel/service.py +104 -0
- package/src/devcouncil/codeintel/store/__init__.py +15 -0
- package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
- package/src/devcouncil/codeintel/sync/__init__.py +19 -0
- package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
- package/src/devcouncil/codeintel/sync/incremental.py +484 -0
- package/src/devcouncil/codeintel/sync/lease.py +96 -0
- package/src/devcouncil/codeintel/sync/scope.py +98 -0
- package/src/devcouncil/council/__init__.py +4 -0
- package/src/devcouncil/council/prompts/__init__.py +4 -0
- package/src/devcouncil/domain/checkpoint_refs.py +17 -0
- package/src/devcouncil/domain/evidence.py +1 -0
- package/src/devcouncil/domain/gap.py +10 -0
- package/src/devcouncil/domain/requirement.py +5 -1
- package/src/devcouncil/domain/task.py +43 -2
- package/src/devcouncil/execution/checkpoints.py +25 -31
- package/src/devcouncil/execution/context_builder.py +15 -44
- package/src/devcouncil/execution/fs_watcher.py +64 -0
- package/src/devcouncil/execution/gated_write.py +203 -0
- package/src/devcouncil/execution/handoff.py +2 -1
- package/src/devcouncil/execution/hook_policy.py +19 -5
- package/src/devcouncil/execution/lease_ops.py +177 -0
- package/src/devcouncil/execution/lease_validation.py +71 -0
- package/src/devcouncil/execution/patch.py +3 -0
- package/src/devcouncil/execution/permissions.py +1 -0
- package/src/devcouncil/execution/policy_engine.py +205 -10
- package/src/devcouncil/execution/prompt_builder.py +278 -33
- package/src/devcouncil/execution/run_trace.py +356 -0
- package/src/devcouncil/execution/shell_session.py +46 -5
- package/src/devcouncil/execution/stop_gate.py +746 -0
- package/src/devcouncil/execution/stop_gate_history.py +113 -0
- package/src/devcouncil/execution/stop_gate_state.py +54 -0
- package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
- package/src/devcouncil/execution/task_gate_ops.py +590 -0
- package/src/devcouncil/execution/task_runner.py +19 -0
- package/src/devcouncil/executors/advisor_tool.py +315 -0
- package/src/devcouncil/executors/agent_registry.py +125 -17
- package/src/devcouncil/executors/claude_sdk.py +376 -0
- package/src/devcouncil/executors/coding_cli.py +724 -25
- package/src/devcouncil/executors/mini_swe.py +50 -8
- package/src/devcouncil/executors/native/agent.py +224 -19
- package/src/devcouncil/executors/openhands.py +50 -8
- package/src/devcouncil/executors/transient_retry.py +99 -0
- package/src/devcouncil/gating/checks/clean_git.py +5 -2
- package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
- package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
- package/src/devcouncil/gating/policy.py +46 -2
- package/src/devcouncil/indexing/ast_matcher.py +41 -4
- package/src/devcouncil/indexing/graph/__init__.py +78 -0
- package/src/devcouncil/indexing/graph/api_routes.py +522 -0
- package/src/devcouncil/indexing/graph/build.py +862 -0
- package/src/devcouncil/indexing/graph/cache.py +329 -0
- package/src/devcouncil/indexing/graph/communities.py +28 -0
- package/src/devcouncil/indexing/graph/cypher.py +107 -0
- package/src/devcouncil/indexing/graph/embeddings.py +194 -0
- package/src/devcouncil/indexing/graph/export.py +381 -0
- package/src/devcouncil/indexing/graph/export_links.py +81 -0
- package/src/devcouncil/indexing/graph/extract_python.py +307 -0
- package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
- package/src/devcouncil/indexing/graph/intel.py +668 -0
- package/src/devcouncil/indexing/graph/liveness.py +992 -0
- package/src/devcouncil/indexing/graph/okf_export.py +65 -0
- package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
- package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
- package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
- package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
- package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
- package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
- package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
- package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
- package/src/devcouncil/indexing/graph/query.py +302 -0
- package/src/devcouncil/indexing/graph/resolve.py +1020 -0
- package/src/devcouncil/indexing/graph/schema.py +103 -0
- package/src/devcouncil/indexing/graph_index.py +20 -29
- package/src/devcouncil/indexing/lsp.py +57 -25
- package/src/devcouncil/indexing/lsp_client.py +577 -0
- package/src/devcouncil/indexing/map_artifacts.py +355 -0
- package/src/devcouncil/indexing/map_refresh.py +141 -0
- package/src/devcouncil/indexing/repo_mapper.py +1509 -138
- package/src/devcouncil/indexing/semantic_index.py +12 -6
- package/src/devcouncil/indexing/subsystem_map.py +163 -0
- package/src/devcouncil/indexing/ts_imports.py +343 -0
- package/src/devcouncil/indexing/viz.py +964 -0
- package/src/devcouncil/indexing/walk.py +52 -0
- package/src/devcouncil/indexing/wiring.py +1776 -0
- package/src/devcouncil/integrations/actions.py +27 -4
- package/src/devcouncil/integrations/check.py +211 -16
- package/src/devcouncil/integrations/claude_assets.py +209 -12
- package/src/devcouncil/integrations/clients/__init__.py +1 -0
- package/src/devcouncil/integrations/clients/aider.py +52 -0
- package/src/devcouncil/integrations/clients/antigravity.py +87 -0
- package/src/devcouncil/integrations/clients/claude.py +339 -0
- package/src/devcouncil/integrations/clients/codex.py +39 -0
- package/src/devcouncil/integrations/clients/common.py +332 -0
- package/src/devcouncil/integrations/clients/cursor.py +164 -0
- package/src/devcouncil/integrations/clients/gemini.py +49 -0
- package/src/devcouncil/integrations/clients/grok.py +105 -0
- package/src/devcouncil/integrations/clients/hooks.py +500 -0
- package/src/devcouncil/integrations/clients/opencode.py +96 -0
- package/src/devcouncil/integrations/clients/warp.py +75 -0
- package/src/devcouncil/integrations/code_review_graph.py +2 -2
- package/src/devcouncil/integrations/github.py +73 -7
- package/src/devcouncil/integrations/integration_cli.py +197 -0
- package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
- package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
- package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
- package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
- package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
- package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
- package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/git.py +281 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -0
- package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
- package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
- package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
- package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
- package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
- package/src/devcouncil/integrations/mcp/handlers/provenance.py +87 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +103 -0
- package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/runs.py +69 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -0
- package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
- package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
- package/src/devcouncil/integrations/mcp/handlers/wiki.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
- package/src/devcouncil/integrations/mcp/server.py +265 -2391
- package/src/devcouncil/integrations/mcp/util.py +325 -0
- package/src/devcouncil/integrations/setup.py +152 -0
- package/src/devcouncil/knowledge/fetch.py +4 -0
- package/src/devcouncil/knowledge/knowledge_select.py +38 -0
- package/src/devcouncil/knowledge/okf.py +2 -1
- package/src/devcouncil/knowledge/resource_discovery.py +40 -0
- package/src/devcouncil/knowledge/wiki.py +643 -0
- package/src/devcouncil/knowledge/wiki_read.py +87 -0
- package/src/devcouncil/live/cards.py +7 -7
- package/src/devcouncil/live/models.py +4 -1
- package/src/devcouncil/live/reviewer.py +90 -11
- package/src/devcouncil/live/signals.py +4 -2
- package/src/devcouncil/live/summary.py +21 -3
- package/src/devcouncil/live/tasks.py +12 -3
- package/src/devcouncil/live/transcripts.py +69 -2
- package/src/devcouncil/llm/cache.py +5 -6
- package/src/devcouncil/llm/model_defaults.yaml +10 -10
- package/src/devcouncil/llm/provider.py +647 -73
- package/src/devcouncil/llm/router.py +271 -46
- package/src/devcouncil/llm/semantic_bridge.py +614 -0
- package/src/devcouncil/optimization/gepa_agent.py +6 -4
- package/src/devcouncil/optimization/skillopt.py +9 -5
- package/src/devcouncil/planning/arbiter_service.py +12 -3
- package/src/devcouncil/planning/correction_manifest.py +107 -10
- package/src/devcouncil/planning/plan_difficulty.py +69 -0
- package/src/devcouncil/planning/plan_service.py +5 -2
- package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
- package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
- package/src/devcouncil/planning/question_conversion.py +56 -0
- package/src/devcouncil/planning/spec_service.py +9 -3
- package/src/devcouncil/repo/ci_scaffold.py +197 -1
- package/src/devcouncil/repo/gitignore.py +1 -2
- package/src/devcouncil/reporting/evidence_export.py +124 -0
- package/src/devcouncil/reporting/evidence_html.py +210 -0
- package/src/devcouncil/reporting/json_report.py +16 -12
- package/src/devcouncil/reporting/markdown_report.py +38 -9
- package/src/devcouncil/reporting/mcp_resources.py +142 -0
- package/src/devcouncil/reporting/report_builder.py +40 -4
- package/src/devcouncil/reporting/task_provenance.py +42 -0
- package/src/devcouncil/reporting/verdict.py +75 -0
- package/src/devcouncil/skills/library/README.md +1 -0
- package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
- package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
- package/src/devcouncil/skills/library/devcouncil.md +93 -0
- package/src/devcouncil/skills/registry.py +43 -12
- package/src/devcouncil/storage/db.py +57 -11
- package/src/devcouncil/storage/models.py +6 -0
- package/src/devcouncil/storage/native.py +5 -3
- package/src/devcouncil/storage/repositories.py +50 -18
- package/src/devcouncil/telemetry/context.py +28 -0
- package/src/devcouncil/telemetry/cost.py +4 -5
- package/src/devcouncil/telemetry/logging_setup.py +78 -11
- package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
- package/src/devcouncil/telemetry/stages.py +27 -2
- package/src/devcouncil/telemetry/tracker.py +50 -13
- package/src/devcouncil/ui/dashboard.py +120 -8
- package/src/devcouncil/utils/fsio.py +58 -0
- package/src/devcouncil/utils/git_snapshot.py +112 -0
- package/src/devcouncil/utils/json_persist.py +53 -0
- package/src/devcouncil/utils/proc.py +89 -0
- package/src/devcouncil/verification/acceptance_compiler.py +36 -13
- package/src/devcouncil/verification/ad_hoc_check.py +95 -3
- package/src/devcouncil/verification/checks/__init__.py +41 -0
- package/src/devcouncil/verification/checks/acceptance.py +39 -0
- package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
- package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
- package/src/devcouncil/verification/checks/command_evidence.py +148 -0
- package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
- package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
- package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
- package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
- package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
- package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
- package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
- package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
- package/src/devcouncil/verification/checks/planned_files.py +98 -0
- package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
- package/src/devcouncil/verification/checks/stale_map.py +80 -0
- package/src/devcouncil/verification/checks/stub_scan.py +71 -0
- package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
- package/src/devcouncil/verification/checks/wiring.py +216 -0
- package/src/devcouncil/verification/claims/__init__.py +23 -0
- package/src/devcouncil/verification/claims/checks.py +395 -0
- package/src/devcouncil/verification/claims/mapper.py +168 -0
- package/src/devcouncil/verification/claims/models.py +39 -0
- package/src/devcouncil/verification/claims/transcript.py +92 -0
- package/src/devcouncil/verification/claims/verdict.py +88 -0
- package/src/devcouncil/verification/command_evidence.py +170 -0
- package/src/devcouncil/verification/command_malformation.py +147 -0
- package/src/devcouncil/verification/command_runner.py +164 -0
- package/src/devcouncil/verification/coverage_measurement.py +292 -0
- package/src/devcouncil/verification/diff_coverage.py +151 -0
- package/src/devcouncil/verification/difficulty.py +296 -0
- package/src/devcouncil/verification/effort_heuristics.py +178 -0
- package/src/devcouncil/verification/gap_ids.py +63 -0
- package/src/devcouncil/verification/gate_cache.py +194 -0
- package/src/devcouncil/verification/gate_selector.py +344 -0
- package/src/devcouncil/verification/git_diff_fallback.py +272 -0
- package/src/devcouncil/verification/implementation_reviewer.py +13 -0
- package/src/devcouncil/verification/incremental_check.py +241 -0
- package/src/devcouncil/verification/next_actions.py +60 -1
- package/src/devcouncil/verification/rigor_analytics.py +130 -0
- package/src/devcouncil/verification/sandbox.py +38 -11
- package/src/devcouncil/verification/stub_detector.py +369 -0
- package/src/devcouncil/verification/test_resolver.py +67 -1
- package/src/devcouncil/verification/verifier.py +137 -1666
- package/src/devcouncil/verification/verify_orchestration.py +610 -0
- package/src/devcouncil/verification/verify_setup.py +176 -0
- package/src/devcouncil/verification/wiki_refresh.py +208 -0
- package/src/semantic_layer/__init__.py +58 -0
- package/src/semantic_layer/benchmark.py +75 -0
- package/src/semantic_layer/cache.py +290 -0
- package/src/semantic_layer/compressor.py +137 -0
- package/src/semantic_layer/config.py +75 -0
- package/src/semantic_layer/embeddings.py +69 -0
- package/src/semantic_layer/llm_backends.py +99 -0
- package/src/semantic_layer/pipeline.py +111 -0
- package/src/semantic_layer/router.py +128 -0
- package/src/semantic_layer/tuner.py +72 -0
- package/uv.lock +973 -9
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/integrations/gitnexus.py +0 -70
- package/src/devcouncil/integrations/graphify.py +0 -34
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Claim-to-check mapper: free-text completion claims → structured assertions.
|
|
2
|
+
|
|
3
|
+
Port of Oversight semantics: sentence-scoped negation/hedging; specific
|
|
4
|
+
assertions suppress GENERIC_DONE; capped to MAX_ASSERTIONS.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from devcouncil.verification.claims.models import Assertion, Kind
|
|
12
|
+
|
|
13
|
+
MAX_ASSERTIONS = 5
|
|
14
|
+
|
|
15
|
+
_SENTENCE_SPLIT = re.compile(r"(?<=[.!?])\s+|\n+")
|
|
16
|
+
|
|
17
|
+
_BENIGN_NEGATIVE = re.compile(
|
|
18
|
+
r"\b(?:0|no|zero|without)\s+(?:\w+\s+)?(?:failures?|errors?|warnings?|regressions?|issues?)\b"
|
|
19
|
+
r"|\bnone\s+failed\b",
|
|
20
|
+
re.I,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_HEDGE = re.compile(
|
|
24
|
+
r"\b(?:not|never|no|fail(?:s|ed|ing|ure)?|should|would|could|might|"
|
|
25
|
+
r"will|going\s+to|plan(?:s|ning)?\s+to|need(?:s)?\s+to|todo|"
|
|
26
|
+
r"haven'?t|hasn'?t|didn'?t|don'?t|doesn'?t|won'?t|can'?t|isn'?t|aren'?t|"
|
|
27
|
+
r"still|yet|broken|remaining\s+work)\b",
|
|
28
|
+
re.I,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_TESTS_PASS = re.compile(
|
|
32
|
+
r"\btests?\s+(?:are\s+|all\s+|now\s+)*pass(?:es|ing)?\b"
|
|
33
|
+
r"|\btest\s+suite\s+passes\b"
|
|
34
|
+
r"|\b(?:pytest|jest|vitest|unittest)\b.*\b(?:green|passed|passing)\b"
|
|
35
|
+
r"|\b\d+\s+(?:tests?\s+)?(?:passed|passing)\b"
|
|
36
|
+
r"|\b(?:test\s+)?suite\s+is\s+green\b"
|
|
37
|
+
r"|\b(?:all\s+)?checks\s+(?:are\s+)?green\b"
|
|
38
|
+
r"|\b(?:unit\s+)?tests\s+succeed(?:ed)?\b"
|
|
39
|
+
r"|\bci\s+is\s+(?:happy|green|passing)\b"
|
|
40
|
+
r"|\beverything\s+passed\b",
|
|
41
|
+
re.I,
|
|
42
|
+
)
|
|
43
|
+
_BUILD = re.compile(
|
|
44
|
+
r"\bbuild\s+(?:succeeds|succeeded|passes|passed|completed\s+successfully|is\s+(?:now\s+)?(?:passing|clean|green))\b"
|
|
45
|
+
r"|\bcompiles?(?:\s+cleanly)?\b"
|
|
46
|
+
r"|\bbuilds\s+cleanly\b"
|
|
47
|
+
r"|\bbuild\s+completed\b",
|
|
48
|
+
re.I,
|
|
49
|
+
)
|
|
50
|
+
_LINT = re.compile(
|
|
51
|
+
r"\blint(?:ing|er)?\s+(?:passes|passed|is\s+clean|clean)\b"
|
|
52
|
+
r"|\bno\s+(?:type|lint(?:ing)?)\s+errors\b"
|
|
53
|
+
r"|\btype-?check(?:ing)?\s+passes\b"
|
|
54
|
+
r"|\bno\s+lint\s+(?:errors|warnings)\b"
|
|
55
|
+
r"|\b(?:mypy|ruff|eslint|flake8)\b.*\b(?:passes|passed|clean)\b",
|
|
56
|
+
re.I,
|
|
57
|
+
)
|
|
58
|
+
_GENERIC_DONE = re.compile(
|
|
59
|
+
r"\b(?:done|completed?|finished|everything\s+works|all\s+set|task\s+is\s+complete|implementation\s+is\s+complete)\b",
|
|
60
|
+
re.I,
|
|
61
|
+
)
|
|
62
|
+
_CREATE_VERB = re.compile(r"\b(?:created|added|wrote|generated|new\s+file)\b", re.I)
|
|
63
|
+
_MODIFY_VERB = re.compile(r"\b(?:updated|edited|modified|changed|fixed|refactored|patched|rewrote)\b", re.I)
|
|
64
|
+
_RAN_COMMAND = re.compile(r"\b(?:ran|executed|invoked)\s+[`\"']([^`\"'\n]+)[`\"'].*?\bsuccess", re.I)
|
|
65
|
+
|
|
66
|
+
_QUOTED_SPAN = re.compile(r"[`\"']([^`\"'\n]+)[`\"']")
|
|
67
|
+
_BARE_PATH = re.compile(r"(?<![\w./\\-])(?:[A-Za-z]:[/\\])?[\w.-]+(?:[/\\][\w.-]+)+(?![\w/\\])")
|
|
68
|
+
_KNOWN_EXTENSIONS = {
|
|
69
|
+
"py", "js", "ts", "tsx", "jsx", "json", "md", "txt", "toml", "yaml", "yml",
|
|
70
|
+
"html", "css", "scss", "sql", "sh", "ps1", "psm1", "bat", "cs", "java",
|
|
71
|
+
"go", "rs", "c", "cpp", "h", "hpp", "rb", "php", "xml", "csv", "ini",
|
|
72
|
+
"cfg", "lock", "env",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _has_known_extension(token: str) -> bool:
|
|
77
|
+
name = token.replace("\\", "/").rsplit("/", 1)[-1]
|
|
78
|
+
if "." not in name:
|
|
79
|
+
return False
|
|
80
|
+
return name.rsplit(".", 1)[-1].lower() in _KNOWN_EXTENSIONS
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _looks_like_path(token: str, bare: bool = False) -> bool:
|
|
84
|
+
token = token.strip()
|
|
85
|
+
if not token or " " in token:
|
|
86
|
+
return False
|
|
87
|
+
if bare:
|
|
88
|
+
return _has_known_extension(token)
|
|
89
|
+
if "/" in token or "\\" in token:
|
|
90
|
+
return True
|
|
91
|
+
return _has_known_extension(token)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _extract_paths(sentence: str) -> list[str]:
|
|
95
|
+
paths: list[str] = []
|
|
96
|
+
seen: set[str] = set()
|
|
97
|
+
for match in _QUOTED_SPAN.finditer(sentence):
|
|
98
|
+
token = match.group(1).strip().rstrip(".,;:!?")
|
|
99
|
+
if _looks_like_path(token) and token not in seen:
|
|
100
|
+
seen.add(token)
|
|
101
|
+
paths.append(token)
|
|
102
|
+
without_quotes = _QUOTED_SPAN.sub(" ", sentence)
|
|
103
|
+
for match in _BARE_PATH.finditer(without_quotes):
|
|
104
|
+
token = match.group(0).rstrip(".,;:!?")
|
|
105
|
+
if _looks_like_path(token, bare=True) and token not in seen:
|
|
106
|
+
seen.add(token)
|
|
107
|
+
paths.append(token)
|
|
108
|
+
return paths
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def map_claims(text: str) -> list[Assertion]:
|
|
112
|
+
"""Parse a completion message into a deduplicated, capped assertion list."""
|
|
113
|
+
if not text or not text.strip():
|
|
114
|
+
return []
|
|
115
|
+
|
|
116
|
+
specific: list[Assertion] = []
|
|
117
|
+
generic: list[Assertion] = []
|
|
118
|
+
seen: set[tuple[Kind, str | None]] = set()
|
|
119
|
+
|
|
120
|
+
def add(bucket: list[Assertion], kind: Kind, target: str | None, source: str) -> None:
|
|
121
|
+
key = (kind, target)
|
|
122
|
+
if key in seen:
|
|
123
|
+
return
|
|
124
|
+
seen.add(key)
|
|
125
|
+
bucket.append(Assertion(kind=kind, target=target, source_text=source.strip()))
|
|
126
|
+
|
|
127
|
+
for sentence in _SENTENCE_SPLIT.split(text):
|
|
128
|
+
sentence = sentence.strip()
|
|
129
|
+
if not sentence:
|
|
130
|
+
continue
|
|
131
|
+
sanitized = _BENIGN_NEGATIVE.sub(" ", sentence)
|
|
132
|
+
if _HEDGE.search(sanitized):
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
if _TESTS_PASS.search(sentence):
|
|
136
|
+
add(specific, Kind.TESTS_PASS, None, sentence)
|
|
137
|
+
if _BUILD.search(sentence):
|
|
138
|
+
add(specific, Kind.BUILD_SUCCEEDS, None, sentence)
|
|
139
|
+
if _LINT.search(sentence):
|
|
140
|
+
add(specific, Kind.LINT_CLEAN, None, sentence)
|
|
141
|
+
|
|
142
|
+
ran = _RAN_COMMAND.search(sentence)
|
|
143
|
+
if ran:
|
|
144
|
+
add(specific, Kind.COMMAND_SUCCEEDED, ran.group(1).strip(), sentence)
|
|
145
|
+
|
|
146
|
+
paths = _extract_paths(sentence)
|
|
147
|
+
if paths:
|
|
148
|
+
if _CREATE_VERB.search(sentence):
|
|
149
|
+
for path in paths:
|
|
150
|
+
add(specific, Kind.FILE_CREATED, path, sentence)
|
|
151
|
+
elif _MODIFY_VERB.search(sentence):
|
|
152
|
+
for path in paths:
|
|
153
|
+
add(specific, Kind.FILE_UPDATED, path, sentence)
|
|
154
|
+
|
|
155
|
+
if _GENERIC_DONE.search(sentence):
|
|
156
|
+
add(generic, Kind.GENERIC_DONE, None, sentence)
|
|
157
|
+
|
|
158
|
+
priority = {
|
|
159
|
+
Kind.TESTS_PASS: 0,
|
|
160
|
+
Kind.BUILD_SUCCEEDS: 1,
|
|
161
|
+
Kind.LINT_CLEAN: 2,
|
|
162
|
+
Kind.COMMAND_SUCCEEDED: 3,
|
|
163
|
+
Kind.FILE_CREATED: 4,
|
|
164
|
+
Kind.FILE_UPDATED: 5,
|
|
165
|
+
}
|
|
166
|
+
specific.sort(key=lambda a: priority.get(a.kind, 9))
|
|
167
|
+
result = specific if specific else generic
|
|
168
|
+
return result[:MAX_ASSERTIONS]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Core data types for claim verification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Kind(str, Enum):
|
|
10
|
+
TESTS_PASS = "tests_pass"
|
|
11
|
+
BUILD_SUCCEEDS = "build_succeeds"
|
|
12
|
+
LINT_CLEAN = "lint_clean"
|
|
13
|
+
FILE_CREATED = "file_created"
|
|
14
|
+
FILE_UPDATED = "file_updated"
|
|
15
|
+
COMMAND_SUCCEEDED = "command_succeeded"
|
|
16
|
+
GENERIC_DONE = "generic_done"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Assertion:
|
|
21
|
+
"""A checkable claim extracted from the agent's completion message."""
|
|
22
|
+
|
|
23
|
+
kind: Kind
|
|
24
|
+
target: str | None = None
|
|
25
|
+
source_text: str = ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Status(str, Enum):
|
|
29
|
+
PASS = "pass"
|
|
30
|
+
FAIL = "fail"
|
|
31
|
+
UNVERIFIABLE = "unverifiable"
|
|
32
|
+
SKIPPED = "skipped"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class CheckResult:
|
|
37
|
+
assertion: Assertion
|
|
38
|
+
status: Status
|
|
39
|
+
detail: str = ""
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Extract the final assistant message from a Claude transcript JSONL."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
_SENTENCE_SPLIT = re.compile(r"(?<=[.!?])\s+")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _text_of(entry: dict) -> str | None:
|
|
13
|
+
message = entry.get("message")
|
|
14
|
+
if not isinstance(message, dict):
|
|
15
|
+
return None
|
|
16
|
+
content = message.get("content")
|
|
17
|
+
if isinstance(content, str):
|
|
18
|
+
return content.strip() or None
|
|
19
|
+
if isinstance(content, list):
|
|
20
|
+
texts = [
|
|
21
|
+
block.get("text", "")
|
|
22
|
+
for block in content
|
|
23
|
+
if isinstance(block, dict) and block.get("type") == "text"
|
|
24
|
+
]
|
|
25
|
+
joined = "\n".join(t for t in texts if t.strip())
|
|
26
|
+
return joined.strip() or None
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def last_assistant_text(path: Path) -> str | None:
|
|
31
|
+
"""Last assistant message that contains text (tool-use-only turns skipped)."""
|
|
32
|
+
try:
|
|
33
|
+
lines = path.read_text(encoding="utf-8-sig", errors="replace").splitlines()
|
|
34
|
+
except OSError:
|
|
35
|
+
return None
|
|
36
|
+
for line in reversed(lines):
|
|
37
|
+
line = line.strip()
|
|
38
|
+
if not line:
|
|
39
|
+
continue
|
|
40
|
+
try:
|
|
41
|
+
entry = json.loads(line)
|
|
42
|
+
except json.JSONDecodeError:
|
|
43
|
+
continue
|
|
44
|
+
if not isinstance(entry, dict) or entry.get("type") != "assistant":
|
|
45
|
+
continue
|
|
46
|
+
text = _text_of(entry)
|
|
47
|
+
if text:
|
|
48
|
+
return text
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def last_assistant_sentence(path: Path) -> str | None:
|
|
53
|
+
"""Last sentence of the last assistant text, or None."""
|
|
54
|
+
text = last_assistant_text(path)
|
|
55
|
+
if not text:
|
|
56
|
+
return None
|
|
57
|
+
parts = [p.strip() for p in _SENTENCE_SPLIT.split(text) if p.strip()]
|
|
58
|
+
return parts[-1] if parts else text.strip()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def ends_on_open_question(path: Path) -> bool:
|
|
62
|
+
"""Heuristic: last assistant turn ends with '?' and no later user turn."""
|
|
63
|
+
try:
|
|
64
|
+
lines = path.read_text(encoding="utf-8-sig", errors="replace").splitlines()
|
|
65
|
+
except OSError:
|
|
66
|
+
return False
|
|
67
|
+
last_assistant_idx = -1
|
|
68
|
+
last_user_idx = -1
|
|
69
|
+
last_assistant_msg: str | None = None
|
|
70
|
+
for i, line in enumerate(lines):
|
|
71
|
+
line = line.strip()
|
|
72
|
+
if not line:
|
|
73
|
+
continue
|
|
74
|
+
try:
|
|
75
|
+
entry = json.loads(line)
|
|
76
|
+
except json.JSONDecodeError:
|
|
77
|
+
continue
|
|
78
|
+
if not isinstance(entry, dict):
|
|
79
|
+
continue
|
|
80
|
+
etype = entry.get("type")
|
|
81
|
+
if etype == "assistant":
|
|
82
|
+
text = _text_of(entry)
|
|
83
|
+
if text:
|
|
84
|
+
last_assistant_idx = i
|
|
85
|
+
last_assistant_msg = text
|
|
86
|
+
elif etype == "user":
|
|
87
|
+
last_user_idx = i
|
|
88
|
+
if last_assistant_idx < 0 or not last_assistant_msg:
|
|
89
|
+
return False
|
|
90
|
+
if last_user_idx > last_assistant_idx:
|
|
91
|
+
return False
|
|
92
|
+
return last_assistant_msg.rstrip().endswith("?")
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Merge claim check results into a corrective message / report."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from devcouncil.verification.claims.models import CheckResult, Status
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ClaimVerdict:
|
|
12
|
+
block: bool
|
|
13
|
+
reason: str
|
|
14
|
+
report: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def render_report(results: list[CheckResult]) -> str:
|
|
18
|
+
if not results:
|
|
19
|
+
return ""
|
|
20
|
+
lines = [
|
|
21
|
+
"# DevCouncil stop-gate claim report",
|
|
22
|
+
"",
|
|
23
|
+
"| Claim | Check | Status |",
|
|
24
|
+
"|---|---|---|",
|
|
25
|
+
]
|
|
26
|
+
for r in results:
|
|
27
|
+
claim = (r.assertion.source_text or r.assertion.kind.value).replace("|", "\\|")
|
|
28
|
+
target = f" `{r.assertion.target}`" if r.assertion.target else ""
|
|
29
|
+
lines.append(f"| {claim} | {r.assertion.kind.value}{target} | {r.status.name} |")
|
|
30
|
+
lines.append("")
|
|
31
|
+
for r in results:
|
|
32
|
+
if r.detail:
|
|
33
|
+
lines.append(f"## {r.assertion.kind.value} — {r.status.name}")
|
|
34
|
+
lines.append(r.detail)
|
|
35
|
+
lines.append("")
|
|
36
|
+
return "\n".join(lines)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_STATUS_ICONS = {
|
|
40
|
+
Status.PASS: "✓",
|
|
41
|
+
Status.FAIL: "✗",
|
|
42
|
+
Status.UNVERIFIABLE: "?",
|
|
43
|
+
Status.SKIPPED: "–",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def summary_line(results: list[CheckResult], *, prefix: str = "🛡 devcouncil") -> str:
|
|
48
|
+
"""One-line verdict summary for Claude systemMessage."""
|
|
49
|
+
if not results:
|
|
50
|
+
return f"{prefix}: (no claims)"
|
|
51
|
+
parts = []
|
|
52
|
+
for r in results:
|
|
53
|
+
icon = _STATUS_ICONS.get(r.status, "?")
|
|
54
|
+
target = f" {r.assertion.target}" if r.assertion.target else ""
|
|
55
|
+
parts.append(f"{r.assertion.kind.value}{target} {icon}")
|
|
56
|
+
return f"{prefix}: " + " | ".join(parts)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _corrective_message(failures: list[CheckResult]) -> str:
|
|
60
|
+
bullets = []
|
|
61
|
+
for r in failures:
|
|
62
|
+
claim = r.assertion.source_text or r.assertion.kind.value
|
|
63
|
+
bullets.append(f'- You claimed: "{claim}" — but verification found: {r.detail}')
|
|
64
|
+
joined = "\n".join(bullets)
|
|
65
|
+
return (
|
|
66
|
+
"CLAIM VERIFICATION FAILED. DevCouncil re-checked your completion claims "
|
|
67
|
+
"against the real environment and found discrepancies:\n\n"
|
|
68
|
+
f"{joined}\n\n"
|
|
69
|
+
"Do not repeat the claim. Investigate the filesystem, git, and command exit "
|
|
70
|
+
"codes, fix the discrepancy, and only finish when these verifications would succeed."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def decide_claims(results: list[CheckResult], blocks_so_far: int, max_blocks: int) -> ClaimVerdict:
|
|
75
|
+
failures = [r for r in results if r.status is Status.FAIL]
|
|
76
|
+
report = render_report(results)
|
|
77
|
+
|
|
78
|
+
if not failures:
|
|
79
|
+
return ClaimVerdict(block=False, reason="", report=report)
|
|
80
|
+
|
|
81
|
+
if blocks_so_far >= max_blocks:
|
|
82
|
+
report += (
|
|
83
|
+
f"\n> Block cap reached ({blocks_so_far}/{max_blocks}): failures above were "
|
|
84
|
+
"NOT re-injected into the agent. Review them yourself.\n"
|
|
85
|
+
)
|
|
86
|
+
return ClaimVerdict(block=False, reason="", report=report)
|
|
87
|
+
|
|
88
|
+
return ClaimVerdict(block=True, reason=_corrective_message(failures), report=report)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Shared helpers for deciding whether a command qualifies as acceptance evidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import shlex
|
|
7
|
+
|
|
8
|
+
# Executables (basename of argv[0]) that ARE test/check runners.
|
|
9
|
+
_RUNNER_EXECUTABLES = frozenset({
|
|
10
|
+
"pytest", "py.test", "vitest", "jest", "mocha", "ava", "tap",
|
|
11
|
+
"unittest", "tox", "nox", "behave", "rspec", "minitest", "phpunit",
|
|
12
|
+
"mypy", "pyright", "tsc",
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
# (argv0 basename, first subcommand) pairs that are runners.
|
|
16
|
+
_RUNNER_SUBCOMMANDS = frozenset({
|
|
17
|
+
("cargo", "test"), ("go", "test"), ("mvn", "test"), ("gradle", "test"),
|
|
18
|
+
("gradlew", "test"), ("make", "test"), ("make", "check"), ("ruff", "check"),
|
|
19
|
+
("swift", "test"), ("dotnet", "test"), ("mix", "test"), ("stack", "test"),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
# Package managers whose ``test`` / ``run <script>`` invocations count.
|
|
23
|
+
_PKG_MANAGERS = frozenset({"npm", "yarn", "pnpm", "bun"})
|
|
24
|
+
_SCRIPT_KEYWORDS = ("test", "typecheck", "type-check", "vitest", "jest", "check")
|
|
25
|
+
|
|
26
|
+
# Interpreters whose inline payloads (-c / -e / --eval) can carry assertions.
|
|
27
|
+
_INLINE_INTERPRETERS = frozenset({"python", "python3", "python2", "py", "node", "deno", "bun"})
|
|
28
|
+
_INLINE_FLAGS = frozenset({"-c", "-e", "--eval"})
|
|
29
|
+
# Assertion-ish content inside an inline payload. ``print(...)`` alone is not
|
|
30
|
+
# evidence; an assert / raise / expect() is.
|
|
31
|
+
_INLINE_EVIDENCE_RE = re.compile(
|
|
32
|
+
r"(\bassert\b|\braise\s+\w+|\bexpect\s*\(|\.to(?:Be|Equal|Throw)\b|\bthrow\s)",
|
|
33
|
+
re.IGNORECASE,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# A single argument token that references a test file/directory. Path-like — a
|
|
37
|
+
# quoted sentence containing the word "test" has spaces and cannot match.
|
|
38
|
+
_TEST_PATH_RE = re.compile(
|
|
39
|
+
r"(^|/)(tests?(/|$)|test_[^/\s]*$|[^/\s]*(?:_tests?|\.test|\.spec)\.[a-z]+$)",
|
|
40
|
+
re.IGNORECASE,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _split(command: str) -> list[str]:
|
|
45
|
+
text = (command or "").strip()
|
|
46
|
+
if not text:
|
|
47
|
+
return []
|
|
48
|
+
try:
|
|
49
|
+
return shlex.split(text)
|
|
50
|
+
except ValueError:
|
|
51
|
+
return text.split()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def command_has_acceptance_evidence(command: str) -> bool:
|
|
55
|
+
"""True when ``command``'s actual INVOCATION can produce behavioral evidence.
|
|
56
|
+
|
|
57
|
+
STRICT, structure-aware allowlist — use for UNTRUSTED input (an agent appending
|
|
58
|
+
expected_tests via ``update_task_scope``). The runner must be the executable, a
|
|
59
|
+
subcommand, a package-manager script, an inline interpreter assertion, or a
|
|
60
|
+
test-file path argument — never just a keyword substring anywhere in the line
|
|
61
|
+
(``echo "tests assert ok"`` must not qualify). For trusted planner/config
|
|
62
|
+
commands at verify time use :func:`command_is_trivial_evidence` instead, whose
|
|
63
|
+
deny-list does not punish legitimate behavioral commands that lack a keyword
|
|
64
|
+
(``make check``, ``./run_smoke.sh``, ``python scripts/validate.py``)."""
|
|
65
|
+
tokens = _split(command)
|
|
66
|
+
if not tokens:
|
|
67
|
+
return False
|
|
68
|
+
base = tokens[0].rsplit("/", 1)[-1].lower()
|
|
69
|
+
if base in _TRIVIAL_EXECUTABLES:
|
|
70
|
+
return False
|
|
71
|
+
if base in _RUNNER_EXECUTABLES:
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
args = tokens[1:]
|
|
75
|
+
lowered_args = [a.lower() for a in args]
|
|
76
|
+
first_arg = lowered_args[0] if lowered_args else ""
|
|
77
|
+
|
|
78
|
+
# Subcommand runners: `cargo test`, `go test ./...`, `ruff check .`, ...
|
|
79
|
+
if (base, first_arg) in _RUNNER_SUBCOMMANDS:
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
# Package-manager scripts: `npm test`, `npm run typecheck`, `yarn vitest`.
|
|
83
|
+
if base in _PKG_MANAGERS and lowered_args:
|
|
84
|
+
script = lowered_args[1] if first_arg == "run" and len(lowered_args) > 1 else first_arg
|
|
85
|
+
if any(keyword in script for keyword in _SCRIPT_KEYWORDS):
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
# Interpreters: `python -m pytest`, `python -c "assert ..."`, `node -e "expect(...)"`.
|
|
89
|
+
if base in _INLINE_INTERPRETERS:
|
|
90
|
+
for i, arg in enumerate(lowered_args):
|
|
91
|
+
if arg == "-m" and i + 1 < len(lowered_args):
|
|
92
|
+
module = lowered_args[i + 1]
|
|
93
|
+
if module in _RUNNER_EXECUTABLES or module == "unittest":
|
|
94
|
+
return True
|
|
95
|
+
if arg in _INLINE_FLAGS and i + 1 < len(args) and _INLINE_EVIDENCE_RE.search(args[i + 1]):
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
# A test-file/dir reference as the executable or an argument: `./run_tests.sh`,
|
|
99
|
+
# `python tests/test_x.py`, `bash tests/run.sh`.
|
|
100
|
+
for token in tokens:
|
|
101
|
+
if " " not in token and _TEST_PATH_RE.search(token):
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Executables whose success proves nothing about the code under review: pure
|
|
108
|
+
# printing, filesystem/system queries, and no-ops. First-token match (after
|
|
109
|
+
# common wrappers) so e.g. ``echo done`` or ``date`` can never coarse-prove an
|
|
110
|
+
# acceptance criterion, while ``make check`` / ``./run_smoke.sh`` still can.
|
|
111
|
+
_TRIVIAL_EXECUTABLES = frozenset({
|
|
112
|
+
"echo", "printf", "true", "false", ":", "exit", "sleep", "ls", "dir", "cat",
|
|
113
|
+
"head", "tail", "wc", "pwd", "cd", "which", "whereis", "whoami", "date",
|
|
114
|
+
"env", "printenv", "type", "touch", "mkdir", "cp", "mv", "hostname", "uname",
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
# Wrapper tokens to skip before resolving the effective executable.
|
|
118
|
+
_WRAPPER_TOKENS = frozenset({"sudo", "time", "nice", "nohup", "uv", "poetry", "pdm", "hatch", "rye", "npx"})
|
|
119
|
+
|
|
120
|
+
# Version/help style flags: a command that only reports its own identity.
|
|
121
|
+
_IDENTITY_FLAGS = frozenset({"--version", "-v", "-version", "--help", "-h", "-help", "version", "--about"})
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def command_is_trivial_evidence(command: str) -> bool:
|
|
125
|
+
"""True when ``command`` cannot possibly prove behavior — deny-list, not allowlist.
|
|
126
|
+
|
|
127
|
+
Use at VERIFY time to filter which passing planner/config commands may
|
|
128
|
+
coarse-prove an acceptance criterion. Deliberately permissive: a behavioral
|
|
129
|
+
command without a test keyword (``make check``, ``python scripts/validate.py``,
|
|
130
|
+
``./run_smoke.sh``) is NOT trivial and keeps its evidential value; only commands
|
|
131
|
+
that provably exercise nothing are rejected:
|
|
132
|
+
|
|
133
|
+
* empty commands;
|
|
134
|
+
* pure printing / system queries (``echo``, ``ls``, ``date``, ...);
|
|
135
|
+
* identity checks (``python --version``, ``node --help``);
|
|
136
|
+
* ``git`` state queries (repo state is not behavior);
|
|
137
|
+
* interpreter one-liners that neither import nor assert nor raise
|
|
138
|
+
(``python -c "print('ok')"``).
|
|
139
|
+
"""
|
|
140
|
+
lowered = (command or "").strip().lower()
|
|
141
|
+
if not lowered:
|
|
142
|
+
return True
|
|
143
|
+
if command_has_acceptance_evidence(lowered):
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
tokens = lowered.split()
|
|
147
|
+
index = 0
|
|
148
|
+
while index < len(tokens) and tokens[index] in _WRAPPER_TOKENS:
|
|
149
|
+
index += 1
|
|
150
|
+
if index < len(tokens) and tokens[index] == "run":
|
|
151
|
+
index += 1
|
|
152
|
+
if index >= len(tokens):
|
|
153
|
+
return True
|
|
154
|
+
executable = tokens[index].rsplit("/", 1)[-1]
|
|
155
|
+
|
|
156
|
+
if executable in _TRIVIAL_EXECUTABLES:
|
|
157
|
+
return True
|
|
158
|
+
# `git status` / `git diff` / `git log` etc.: repository state, not behavior.
|
|
159
|
+
if executable == "git":
|
|
160
|
+
return True
|
|
161
|
+
# Bare identity check: every remaining argument is a version/help flag.
|
|
162
|
+
rest = tokens[index + 1:]
|
|
163
|
+
if rest and all(tok in _IDENTITY_FLAGS for tok in rest):
|
|
164
|
+
return True
|
|
165
|
+
# Interpreter one-liner that neither imports the code, asserts, raises, nor
|
|
166
|
+
# calls anything that could fail meaningfully: `python -c "print('ok')"`.
|
|
167
|
+
if executable.startswith(("python", "node", "ruby", "perl")) and (" -c " in f" {lowered} " or " -e " in f" {lowered} "):
|
|
168
|
+
if not any(marker in lowered for marker in ("import", "require", "assert", "raise", "exit(", "sys.exit")):
|
|
169
|
+
return True
|
|
170
|
+
return False
|