Dev10x 0.77.0__tar.gz → 0.78.0__tar.gz
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.
- {dev10x-0.77.0/src/Dev10x.egg-info → dev10x-0.78.0}/PKG-INFO +1 -1
- {dev10x-0.77.0 → dev10x-0.78.0}/pyproject.toml +12 -2
- {dev10x-0.77.0 → dev10x-0.78.0/src/Dev10x.egg-info}/PKG-INFO +1 -1
- {dev10x-0.77.0 → dev10x-0.78.0}/src/Dev10x.egg-info/SOURCES.txt +10 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/permission.py +30 -6
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/skill.py +23 -33
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/friction_level.py +43 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/install_version.py +51 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/session_rules.py +3 -17
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/edit_validator.py +35 -4
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/session.py +2 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/session_dispatch.py +39 -0
- dev10x-0.78.0/src/dev10x/mcp/_app.py +76 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/git_tools.py +60 -4
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/github_tools.py +26 -1
- dev10x-0.78.0/src/dev10x/mcp/knowledge_prompts.py +85 -0
- dev10x-0.78.0/src/dev10x/mcp/knowledge_resources.py +132 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/misc_tools.py +23 -1
- dev10x-0.78.0/src/dev10x/mcp/resource_watcher.py +413 -0
- dev10x-0.78.0/src/dev10x/mcp/roots_manager.py +277 -0
- dev10x-0.78.0/src/dev10x/mcp/roots_tools.py +41 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/server_cli.py +9 -0
- dev10x-0.78.0/src/dev10x/skills/harvest/__init__.py +7 -0
- dev10x-0.78.0/src/dev10x/skills/harvest/closed_prs.py +150 -0
- dev10x-0.78.0/src/dev10x/skills/harvest/review_threads.py +195 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/monitor/pr_notify.py +10 -29
- dev10x-0.78.0/src/dev10x/skills/notifications/slack_notify.py +312 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/subprocess_utils.py +46 -3
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/__init__.py +7 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/skill_redirect.py +18 -19
- dev10x-0.78.0/src/dev10x/validators/spec_drift.py +185 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_subprocess_utils.py +87 -0
- dev10x-0.77.0/src/dev10x/mcp/_app.py +0 -11
- {dev10x-0.77.0 → dev10x-0.78.0}/LICENSE +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/README.md +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/setup.cfg +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/Dev10x.egg-info/dependency_links.txt +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/Dev10x.egg-info/entry_points.txt +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/Dev10x.egg-info/requires.txt +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/Dev10x.egg-info/top_level.txt +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/audit/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/audit/analyze.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/audit/log_reader.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/audit/permissions_model.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/cli.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/config.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/github_app.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/github_app_api.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/hook.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/init.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/platform.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/playbook.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/validate.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/config/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/config/loader.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/db/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/audit_writer.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/claude_paths.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/common/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/common/allow_rule.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/common/branch_name.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/common/repository_ref.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/common/result.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/common/skill_name.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/common/ticket_id.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/config_loader.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/dev10x_paths.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/documents/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/documents/config_document.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/documents/plan.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/documents/session_state.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/documents/settings_document.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/documents/task.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/events/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/events/hook_input.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/file_locks.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/git_context.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/hook_telemetry.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/profile_tier.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/rules/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/rules/policy_rule.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/rules/rule_engine.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/rules/sql.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/rules/validation_rule.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/sensitivity.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/session_document.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/git/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/github/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/github/app_auth.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/github/slack.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/audit_emit.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/permission_diagnostics.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/session_place.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/session_policy.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/skill.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/task_plan_sync.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/audit_tools.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/daemon.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/plan_tools.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/server_db.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/session_store.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/transport.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/wiring.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/monitor/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/permission/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/plan/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/plan/service.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/platform/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/platform/registry.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/release/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/runner/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/scope/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/scope/_matcher.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/scope/index_parser.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/scope/norms_renderer.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/scope/safeguards_renderer.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/session/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/session/queries.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skill_index/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skill_index/builder.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/audit/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/audit/analyze_actions.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/audit/analyze_permissions.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/audit/cli_friction.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/audit/extract_session.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/audit/instruction_budget.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/audit/privacy.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/common/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/common/jtbd.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/database/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/registry.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategies/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategies/forbidden_token_priming.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategies/mcp_horizontal_duplicates.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategies/mcp_vs_script_drift.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategies/missing_linear_mcp_allow.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategy.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/evidence/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/git_fixup/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/git_fixup/find_fixup_target.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/monitor/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/monitor/ci_check_status.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/notifications/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/notifications/slack_review_request.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/orchestration/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/orchestration/batch_detection.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/orchestration/subagent_protocol.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/backup.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/baseline-permissions.yaml +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/clean_project_files.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/cluster_paths.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/config.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/doctor.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/enumerate_mcp.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/file_lock.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/local_skill_approval.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/merge_worktree_permissions.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/update_paths.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/uv_run_project.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission_investigator/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission_investigator/fixtures.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission_investigator/matrix.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission_investigator/report.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/playbook/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/playbook/compare.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/playbook/discovery.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/playbook/report.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/release/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/release/classifier.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/release/collect_prs.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/spec/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/spec/drift_detector.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/utilities/__init__.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/_quote_strip.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/base.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/bash_aggregation.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/command-skill-map.yaml +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/command_substitution.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/commit_jtbd.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/execution_safety.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/mcp_prefix.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/pipeline_allow.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/pr_base.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/prefix_friction.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/redundant_fetch.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/registry.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/safe_expansion.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/safe_subshell.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/sensitivity_target.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/sql_safety.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_cli.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_cli_lazy.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_cwd_discipline_sweep.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_cwd_enforcement.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_integration.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_no_module_scope_gitcontext.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_script_loadability.py +0 -0
- {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_shell_script_flags.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "Dev10x"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.78.0"
|
|
4
4
|
description = "Claude Code plugin providing reusable skills, hooks, and commands"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
@@ -63,7 +63,7 @@ addopts = "--cov=src/dev10x --cov-report=term-missing"
|
|
|
63
63
|
source = ["src/dev10x"]
|
|
64
64
|
|
|
65
65
|
[tool.coverage.report]
|
|
66
|
-
fail_under =
|
|
66
|
+
fail_under = 75
|
|
67
67
|
show_missing = true
|
|
68
68
|
exclude_lines = ["if __name__", "pragma: no cover"]
|
|
69
69
|
|
|
@@ -74,5 +74,15 @@ src = ["src"]
|
|
|
74
74
|
[tool.ruff.lint]
|
|
75
75
|
select = ["E", "F", "I", "UP"]
|
|
76
76
|
|
|
77
|
+
[tool.pytest_benchmark]
|
|
78
|
+
# Store baselines in .benchmarks/ (gitignored per-run, cached by CI)
|
|
79
|
+
storage = "file"
|
|
80
|
+
# Fail on mean regression > 20% vs stored baseline
|
|
81
|
+
compare_fail = ["mean:20%"]
|
|
82
|
+
# Minimum rounds so outliers don't skew results
|
|
83
|
+
min_rounds = 5
|
|
84
|
+
timer = "perf_counter"
|
|
85
|
+
disable_gc = true
|
|
86
|
+
|
|
77
87
|
[tool.mypy]
|
|
78
88
|
mypy_path = "src"
|
|
@@ -82,8 +82,13 @@ src/dev10x/mcp/audit_tools.py
|
|
|
82
82
|
src/dev10x/mcp/daemon.py
|
|
83
83
|
src/dev10x/mcp/git_tools.py
|
|
84
84
|
src/dev10x/mcp/github_tools.py
|
|
85
|
+
src/dev10x/mcp/knowledge_prompts.py
|
|
86
|
+
src/dev10x/mcp/knowledge_resources.py
|
|
85
87
|
src/dev10x/mcp/misc_tools.py
|
|
86
88
|
src/dev10x/mcp/plan_tools.py
|
|
89
|
+
src/dev10x/mcp/resource_watcher.py
|
|
90
|
+
src/dev10x/mcp/roots_manager.py
|
|
91
|
+
src/dev10x/mcp/roots_tools.py
|
|
87
92
|
src/dev10x/mcp/server_cli.py
|
|
88
93
|
src/dev10x/mcp/server_db.py
|
|
89
94
|
src/dev10x/mcp/session_store.py
|
|
@@ -128,10 +133,14 @@ src/dev10x/skills/doctor/strategies/missing_linear_mcp_allow.py
|
|
|
128
133
|
src/dev10x/skills/evidence/__init__.py
|
|
129
134
|
src/dev10x/skills/git_fixup/__init__.py
|
|
130
135
|
src/dev10x/skills/git_fixup/find_fixup_target.py
|
|
136
|
+
src/dev10x/skills/harvest/__init__.py
|
|
137
|
+
src/dev10x/skills/harvest/closed_prs.py
|
|
138
|
+
src/dev10x/skills/harvest/review_threads.py
|
|
131
139
|
src/dev10x/skills/monitor/__init__.py
|
|
132
140
|
src/dev10x/skills/monitor/ci_check_status.py
|
|
133
141
|
src/dev10x/skills/monitor/pr_notify.py
|
|
134
142
|
src/dev10x/skills/notifications/__init__.py
|
|
143
|
+
src/dev10x/skills/notifications/slack_notify.py
|
|
135
144
|
src/dev10x/skills/notifications/slack_review_request.py
|
|
136
145
|
src/dev10x/skills/orchestration/__init__.py
|
|
137
146
|
src/dev10x/skills/orchestration/batch_detection.py
|
|
@@ -181,6 +190,7 @@ src/dev10x/validators/safe_expansion.py
|
|
|
181
190
|
src/dev10x/validators/safe_subshell.py
|
|
182
191
|
src/dev10x/validators/sensitivity_target.py
|
|
183
192
|
src/dev10x/validators/skill_redirect.py
|
|
193
|
+
src/dev10x/validators/spec_drift.py
|
|
184
194
|
src/dev10x/validators/sql_safety.py
|
|
185
195
|
tests/test_cli.py
|
|
186
196
|
tests/test_cli_lazy.py
|
|
@@ -310,12 +310,31 @@ def init() -> None:
|
|
|
310
310
|
default=False,
|
|
311
311
|
help=(
|
|
312
312
|
"Do not remove project rules that are exact duplicates of global rules. "
|
|
313
|
-
"
|
|
314
|
-
"
|
|
313
|
+
"Retained for backward compatibility — global-dedup is now OFF by "
|
|
314
|
+
"default, so this flag is only meaningful alongside --aggressive."
|
|
315
|
+
),
|
|
316
|
+
)
|
|
317
|
+
@click.option(
|
|
318
|
+
"--aggressive",
|
|
319
|
+
is_flag=True,
|
|
320
|
+
default=False,
|
|
321
|
+
help=(
|
|
322
|
+
"Also remove project rules that are exact duplicates of global rules. "
|
|
323
|
+
"OFF by default (finding #47): global→project rule inheritance is NOT "
|
|
324
|
+
"guaranteed when a project has its own settings.local.json, so stripping "
|
|
325
|
+
"the local copy can reintroduce permission prompts. Only enable after "
|
|
326
|
+
"`dev10x permission investigate` confirms inheritance holds for this "
|
|
327
|
+
"environment. Recover a bad run with `dev10x permission clean --restore`."
|
|
315
328
|
),
|
|
316
329
|
)
|
|
317
330
|
def clean(
|
|
318
|
-
*,
|
|
331
|
+
*,
|
|
332
|
+
dry_run: bool,
|
|
333
|
+
verbose: bool,
|
|
334
|
+
summary: bool,
|
|
335
|
+
restore: bool,
|
|
336
|
+
skip_global_dedup: bool,
|
|
337
|
+
aggressive: bool,
|
|
319
338
|
) -> None:
|
|
320
339
|
"""Clean redundant permissions from project settings files."""
|
|
321
340
|
from dev10x.skills.permission import clean_project_files as mod
|
|
@@ -323,6 +342,11 @@ def clean(
|
|
|
323
342
|
if restore:
|
|
324
343
|
sys.exit(mod._restore(config_path=mod.find_config()))
|
|
325
344
|
|
|
345
|
+
# Global-dedup is opt-in (finding #47). It runs only under --aggressive,
|
|
346
|
+
# and --skip-global-dedup always wins so the safe behavior cannot be
|
|
347
|
+
# accidentally re-enabled.
|
|
348
|
+
skip_global_dedup = skip_global_dedup or not aggressive
|
|
349
|
+
|
|
326
350
|
config_path = mod.find_config()
|
|
327
351
|
click.echo(f"Config: {config_path}")
|
|
328
352
|
config = mod.load_config(config_path)
|
|
@@ -407,9 +431,9 @@ def clean(
|
|
|
407
431
|
if total_global_dedup > 0 and not skip_global_dedup:
|
|
408
432
|
click.echo(
|
|
409
433
|
f"\n⚠ WARNING: {total_global_dedup} rules removed as exact duplicates of global"
|
|
410
|
-
" rules. Global→project rule inheritance is NOT guaranteed when a
|
|
411
|
-
" own settings.local.json (finding #47).
|
|
412
|
-
"
|
|
434
|
+
" rules (--aggressive). Global→project rule inheritance is NOT guaranteed when a"
|
|
435
|
+
" project has its own settings.local.json (finding #47). If new permission prompts"
|
|
436
|
+
" appear, recover with `dev10x permission clean --restore`."
|
|
413
437
|
)
|
|
414
438
|
|
|
415
439
|
if total_secrets > 0:
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import subprocess
|
|
4
3
|
import sys
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
|
|
@@ -23,14 +22,6 @@ def notify() -> None:
|
|
|
23
22
|
"""
|
|
24
23
|
|
|
25
24
|
|
|
26
|
-
def _plugin_root() -> Path:
|
|
27
|
-
"""Resolve the plugin root containing the skills/ directory.
|
|
28
|
-
|
|
29
|
-
`src/dev10x/commands/skill.py` -> parents[3] = plugin root.
|
|
30
|
-
"""
|
|
31
|
-
return Path(__file__).resolve().parents[3]
|
|
32
|
-
|
|
33
|
-
|
|
34
25
|
@notify.command(name="slack-review-prepare")
|
|
35
26
|
@click.option("--pr", type=int, required=True, help="PR number")
|
|
36
27
|
@click.option("--repo", required=True, help="GitHub repo (owner/name)")
|
|
@@ -69,37 +60,36 @@ def slack_send(
|
|
|
69
60
|
thread_ts: str | None,
|
|
70
61
|
workspace: str | None,
|
|
71
62
|
) -> None:
|
|
72
|
-
"""Send a Slack message via the
|
|
63
|
+
"""Send a Slack message via the importable slack_notify module (GH-442).
|
|
73
64
|
|
|
74
|
-
Delegates to `skills
|
|
75
|
-
|
|
76
|
-
|
|
65
|
+
Delegates to `dev10x.skills.notifications.slack_notify` so the command
|
|
66
|
+
works when dev10x is installed via ``uvx`` — where ``skills/`` data files
|
|
67
|
+
are not shipped as part of the wheel and cannot be reached by filesystem
|
|
68
|
+
traversal from the installed package location.
|
|
77
69
|
"""
|
|
78
70
|
if not message and not message_file:
|
|
79
71
|
raise click.UsageError("Provide --message or --message-file.")
|
|
80
72
|
|
|
81
|
-
|
|
82
|
-
if not slack_notify.exists():
|
|
83
|
-
click.echo(f"slack-notify.py not found at {slack_notify}", err=True)
|
|
84
|
-
sys.exit(1)
|
|
73
|
+
from dev10x.skills.notifications import slack_notify
|
|
85
74
|
|
|
86
|
-
cmd: list[str] = [str(slack_notify), "--channel", channel]
|
|
87
|
-
if message_file is not None:
|
|
88
|
-
cmd.extend(["--message-file", str(message_file)])
|
|
89
|
-
if message is not None:
|
|
90
|
-
cmd.extend(["--message", message])
|
|
91
|
-
if thread_ts is not None:
|
|
92
|
-
cmd.extend(["--thread-ts", thread_ts])
|
|
93
75
|
if workspace is not None:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
76
|
+
slack_notify.set_workspace(workspace)
|
|
77
|
+
|
|
78
|
+
msg: str
|
|
79
|
+
if message_file is not None:
|
|
80
|
+
msg = message_file.read_text()
|
|
81
|
+
else:
|
|
82
|
+
msg = message # type: ignore[assignment] # validated above
|
|
83
|
+
|
|
84
|
+
ts = slack_notify.send_slack_message(
|
|
85
|
+
channel=channel,
|
|
86
|
+
message=msg,
|
|
87
|
+
thread_ts=thread_ts,
|
|
88
|
+
)
|
|
89
|
+
if ts:
|
|
90
|
+
click.echo(f"✅ Slack message sent successfully! ts={ts}")
|
|
91
|
+
else:
|
|
92
|
+
sys.exit(1)
|
|
103
93
|
|
|
104
94
|
|
|
105
95
|
@skill.command(name="count-instructions")
|
|
@@ -54,5 +54,48 @@ class FrictionLevel(StrEnum):
|
|
|
54
54
|
return member
|
|
55
55
|
return cls.default()
|
|
56
56
|
|
|
57
|
+
def pending_decisions_guidance(self) -> str:
|
|
58
|
+
"""Return the session-resume instruction for pending-decision state.
|
|
59
|
+
|
|
60
|
+
Called by ``DecisionGuidanceRule`` when the plan has one or more
|
|
61
|
+
unresolved decision tasks. Each member encodes its own pacing
|
|
62
|
+
rule, eliminating the if/elif dispatch from the rule body.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
A non-empty instruction string for every recognised member.
|
|
66
|
+
"""
|
|
67
|
+
if self is FrictionLevel.ADAPTIVE:
|
|
68
|
+
return (
|
|
69
|
+
"Session resumed with pending decisions. Friction level is adaptive — "
|
|
70
|
+
"auto-select recommended options for all queued decisions and continue "
|
|
71
|
+
"advancing through the task list without calling AskUserQuestion."
|
|
72
|
+
)
|
|
73
|
+
return (
|
|
74
|
+
"Session resumed with pending decisions. "
|
|
75
|
+
"Re-ask each pending decision using AskUserQuestion — "
|
|
76
|
+
"invoke Dev10x:ask before advancing."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def fallback_guidance(self, *, fallback: str) -> str:
|
|
80
|
+
"""Return a friction-level-specific fallback clause for block messages.
|
|
81
|
+
|
|
82
|
+
In GUIDED mode the agent is shown a concrete fallback path (skill
|
|
83
|
+
guardrails to apply manually, or an MCP-unavailability escape).
|
|
84
|
+
In STRICT and ADAPTIVE modes the block is unadorned — the fallback
|
|
85
|
+
text is omitted so the message stays concise.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
fallback: The fallback snippet to surface in GUIDED mode.
|
|
89
|
+
May be an MCP-server fallback description, a manual-
|
|
90
|
+
guardrail list, or any other escape-hatch hint.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The fallback clause (including a leading newline) when GUIDED,
|
|
94
|
+
or an empty string for other levels.
|
|
95
|
+
"""
|
|
96
|
+
if self is FrictionLevel.GUIDED:
|
|
97
|
+
return fallback
|
|
98
|
+
return ""
|
|
99
|
+
|
|
57
100
|
|
|
58
101
|
__all__ = ["FrictionLevel"]
|
|
@@ -85,6 +85,55 @@ def write_applied_version(
|
|
|
85
85
|
return path
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
def read_running_hook_version(*, plugin_root: Path | None = None) -> str | None:
|
|
89
|
+
"""Return the version that is *currently executing* inside this session.
|
|
90
|
+
|
|
91
|
+
``$CLAUDE_PLUGIN_ROOT`` is set by Claude Code to the plugin directory
|
|
92
|
+
that was resolved when the session started. Hooks loaded from that
|
|
93
|
+
directory run for the entire session lifetime — an on-disk upgrade
|
|
94
|
+
does not swap them out. Reading the version from this directory gives
|
|
95
|
+
us the *running* version, which may lag behind the latest installed
|
|
96
|
+
version when the user ran ``claude plugin update`` mid-session.
|
|
97
|
+
|
|
98
|
+
Returns ``None`` when the running root is unknown (e.g., local
|
|
99
|
+
``--plugin-dir`` dev runs that do not carry a versioned cache path).
|
|
100
|
+
"""
|
|
101
|
+
root = plugin_root or _default_plugin_root()
|
|
102
|
+
return read_plugin_version(plugin_root=root)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def read_latest_installed_version(
|
|
106
|
+
*,
|
|
107
|
+
cache_dir: Path | None = None,
|
|
108
|
+
publisher: str = "Dev10x-Guru",
|
|
109
|
+
plugin_slug: str = "dev10x-claude",
|
|
110
|
+
) -> str | None:
|
|
111
|
+
"""Return the highest-version directory under the plugin cache.
|
|
112
|
+
|
|
113
|
+
Scans ``~/.claude/plugins/cache/<publisher>/<plugin_slug>/`` for
|
|
114
|
+
semver-named subdirectories and returns the latest. Returns ``None``
|
|
115
|
+
when the directory is absent or empty (e.g., ``--plugin-dir`` installs
|
|
116
|
+
that bypass the cache).
|
|
117
|
+
"""
|
|
118
|
+
from dev10x.domain.claude_paths import ClaudeDir
|
|
119
|
+
|
|
120
|
+
root = cache_dir or (ClaudeDir.plugins_cache_dir() / publisher / plugin_slug)
|
|
121
|
+
if not root.is_dir():
|
|
122
|
+
return None
|
|
123
|
+
versions = sorted(
|
|
124
|
+
(d for d in root.iterdir() if d.is_dir()),
|
|
125
|
+
key=lambda p: _version_tuple(p.name),
|
|
126
|
+
)
|
|
127
|
+
return versions[-1].name if versions else None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _version_tuple(version: str) -> tuple[int, ...]:
|
|
131
|
+
try:
|
|
132
|
+
return tuple(int(x) for x in version.split("."))
|
|
133
|
+
except ValueError:
|
|
134
|
+
return (0,)
|
|
135
|
+
|
|
136
|
+
|
|
88
137
|
def record_upgrade(*, version: str | None = None) -> Result[dict[str, Any]]:
|
|
89
138
|
"""Record the currently-installed plugin version as applied.
|
|
90
139
|
|
|
@@ -169,7 +218,9 @@ __all__ = [
|
|
|
169
218
|
"InstallState",
|
|
170
219
|
"install_state",
|
|
171
220
|
"read_applied_version",
|
|
221
|
+
"read_latest_installed_version",
|
|
172
222
|
"read_plugin_version",
|
|
223
|
+
"read_running_hook_version",
|
|
173
224
|
"record_upgrade",
|
|
174
225
|
"write_applied_version",
|
|
175
226
|
]
|
|
@@ -69,26 +69,12 @@ class DecisionGuidanceRule(PolicyRule[str]):
|
|
|
69
69
|
raise UnknownFrictionLevelError(f"Unknown friction level: {self.friction_level!r}")
|
|
70
70
|
|
|
71
71
|
summary = PlanSummary.from_dict(data=self.plan)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
has_remaining = bool(summary.pending_tasks)
|
|
75
|
-
if has_remaining:
|
|
72
|
+
if not summary.pending_decisions:
|
|
73
|
+
if summary.pending_tasks:
|
|
76
74
|
return "Session resumed with tasks remaining. Auto-advance through the task list."
|
|
77
75
|
return ""
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
"Session resumed with pending decisions. Friction level is adaptive — "
|
|
82
|
-
"auto-select recommended options for all queued decisions and continue "
|
|
83
|
-
"advancing through the task list without calling AskUserQuestion."
|
|
84
|
-
)
|
|
85
|
-
if self.friction_level in (FrictionLevel.STRICT, FrictionLevel.GUIDED):
|
|
86
|
-
return (
|
|
87
|
-
"Session resumed with pending decisions. "
|
|
88
|
-
"Re-ask each pending decision using AskUserQuestion — "
|
|
89
|
-
"invoke Dev10x:ask before advancing."
|
|
90
|
-
)
|
|
91
|
-
raise UnknownFrictionLevelError(f"Unhandled friction level: {self.friction_level!r}")
|
|
77
|
+
return self.friction_level.pending_decisions_guidance()
|
|
92
78
|
|
|
93
79
|
|
|
94
80
|
__all__ = [
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
"""Edit/Write tool validator — blocks sensitive file writes.
|
|
1
|
+
"""Edit/Write tool validator — blocks sensitive file writes and spec drift.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Two validation passes:
|
|
4
|
+
|
|
5
|
+
1. YAML-rule pass: loads rules from command-skill-map.yaml where
|
|
6
|
+
matcher="Edit|Write", delegates evaluation to RuleEngine.
|
|
7
|
+
First block wins.
|
|
8
|
+
|
|
9
|
+
2. Python-validator pass: runs active Edit|Write validators from the
|
|
10
|
+
shared ValidatorRegistry (e.g. SpecDriftValidator / DX015).
|
|
11
|
+
Only validators whose ``should_run()`` returns True participate.
|
|
5
12
|
"""
|
|
6
13
|
|
|
7
14
|
from __future__ import annotations
|
|
@@ -10,7 +17,7 @@ import sys
|
|
|
10
17
|
from pathlib import Path
|
|
11
18
|
from typing import TYPE_CHECKING, Any
|
|
12
19
|
|
|
13
|
-
from dev10x.domain.events.hook_input import HookResult
|
|
20
|
+
from dev10x.domain.events.hook_input import HookInput, HookResult
|
|
14
21
|
|
|
15
22
|
if TYPE_CHECKING:
|
|
16
23
|
from dev10x.domain.rules.rule_engine import RuleEngine
|
|
@@ -26,6 +33,28 @@ def _build_engine(*, yaml_path: Path) -> RuleEngine:
|
|
|
26
33
|
return RuleEngine.from_config(config=config)
|
|
27
34
|
|
|
28
35
|
|
|
36
|
+
def _run_python_validators(*, data: dict[str, Any], debug: bool = False) -> None:
|
|
37
|
+
"""Run Python validators that handle Edit|Write tool calls.
|
|
38
|
+
|
|
39
|
+
Mirrors the dispatch pattern in ``commands/hook.py:_validate_bash_body``
|
|
40
|
+
but filters to validators whose ``should_run`` returns True for the
|
|
41
|
+
given Edit|Write input. YAML-rule blocks have already been handled
|
|
42
|
+
before this function is called.
|
|
43
|
+
"""
|
|
44
|
+
from dev10x.validators import get_chain
|
|
45
|
+
|
|
46
|
+
inp = HookInput.from_dict(data=data)
|
|
47
|
+
for result in get_chain().run(inp=inp):
|
|
48
|
+
if debug:
|
|
49
|
+
import sys as _sys
|
|
50
|
+
|
|
51
|
+
print(
|
|
52
|
+
f"[DEBUG] Python validator blocked: {result}",
|
|
53
|
+
file=_sys.stderr,
|
|
54
|
+
)
|
|
55
|
+
result.emit()
|
|
56
|
+
|
|
57
|
+
|
|
29
58
|
def validate_edit_write(
|
|
30
59
|
*,
|
|
31
60
|
data: dict[str, Any],
|
|
@@ -55,4 +84,6 @@ def validate_edit_write(
|
|
|
55
84
|
)
|
|
56
85
|
HookResult(message=match.message).emit()
|
|
57
86
|
|
|
87
|
+
_run_python_validators(data=data, debug=debug)
|
|
88
|
+
|
|
58
89
|
sys.exit(0)
|
|
@@ -26,6 +26,7 @@ from dev10x.domain.session_document import (
|
|
|
26
26
|
from dev10x.hooks.session_dispatch import (
|
|
27
27
|
build_autonomy_reassurance_context, # noqa: F401 — re-exported via __all__
|
|
28
28
|
build_guidance_context, # noqa: F401 — re-exported via __all__
|
|
29
|
+
build_hook_version_drift_context, # noqa: F401 — re-exported via __all__
|
|
29
30
|
build_install_check_context, # noqa: F401 — re-exported via __all__
|
|
30
31
|
build_reload_context, # noqa: F401 — re-exported via __all__
|
|
31
32
|
context_compact, # noqa: F401 — re-exported via __all__
|
|
@@ -73,6 +74,7 @@ def _format_decision_guidance(*, plan, friction_level):
|
|
|
73
74
|
__all__ = [
|
|
74
75
|
"build_autonomy_reassurance_context",
|
|
75
76
|
"build_guidance_context",
|
|
77
|
+
"build_hook_version_drift_context",
|
|
76
78
|
"build_install_check_context",
|
|
77
79
|
"build_reload_context",
|
|
78
80
|
"context_compact",
|
|
@@ -139,6 +139,44 @@ def build_install_check_context() -> str:
|
|
|
139
139
|
return ""
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
def build_hook_version_drift_context() -> str:
|
|
143
|
+
"""Warn when the running-hook version lags the latest installed version.
|
|
144
|
+
|
|
145
|
+
Claude Code loads hooks once at session start from ``$CLAUDE_PLUGIN_ROOT``.
|
|
146
|
+
An on-disk ``claude plugin update`` installs a newer version but does NOT
|
|
147
|
+
swap the running hooks — the session continues executing the pre-upgrade
|
|
148
|
+
hooks until it restarts. This means shipped friction fixes, new validators,
|
|
149
|
+
and catalog improvements are dormant in long-running sessions.
|
|
150
|
+
|
|
151
|
+
This check is **distinct** from :func:`build_install_check_context`, which
|
|
152
|
+
compares the installed version against the last-applied upgrade-cleanup
|
|
153
|
+
version. That check detects settings staleness; this one detects
|
|
154
|
+
running-hook staleness — they can diverge when settings were refreshed
|
|
155
|
+
but the session was not restarted.
|
|
156
|
+
|
|
157
|
+
Returns an empty string when no drift is detected or when either version
|
|
158
|
+
cannot be determined (``--plugin-dir`` dev installs, new users, etc.).
|
|
159
|
+
"""
|
|
160
|
+
from dev10x.domain.install_version import (
|
|
161
|
+
read_latest_installed_version,
|
|
162
|
+
read_running_hook_version,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
running = read_running_hook_version()
|
|
166
|
+
if running is None:
|
|
167
|
+
return ""
|
|
168
|
+
latest = read_latest_installed_version()
|
|
169
|
+
if latest is None:
|
|
170
|
+
return ""
|
|
171
|
+
if running == latest:
|
|
172
|
+
return ""
|
|
173
|
+
return (
|
|
174
|
+
f"Dev10x hooks running v{running} but v{latest} is installed on disk.\n"
|
|
175
|
+
"Restart this session (or run `/Dev10x:upgrade-cleanup`) to activate "
|
|
176
|
+
"shipped friction fixes, validators, and catalog improvements."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
142
180
|
def session_install_check() -> None:
|
|
143
181
|
"""Emit install-state guidance as additionalContext (SessionStart hook)."""
|
|
144
182
|
content = build_install_check_context()
|
|
@@ -233,6 +271,7 @@ def session_goodbye(data: dict | None = None) -> None:
|
|
|
233
271
|
|
|
234
272
|
|
|
235
273
|
__all__ = [
|
|
274
|
+
"build_hook_version_drift_context",
|
|
236
275
|
"build_install_check_context",
|
|
237
276
|
"build_reload_context",
|
|
238
277
|
"build_autonomy_reassurance_context",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Shared FastMCP application instance for the Dev10x CLI server.
|
|
2
|
+
|
|
3
|
+
Split out of server_cli.py (GH-243/A6) so per-domain tool modules
|
|
4
|
+
register against one server without a circular import.
|
|
5
|
+
|
|
6
|
+
GH-341: A lifespan context manager starts the knowledge-resource file
|
|
7
|
+
watcher and wires it to the active MCP session so that
|
|
8
|
+
``notifications/resources/list_changed`` and
|
|
9
|
+
``notifications/resources/updated`` are emitted to clients whenever the
|
|
10
|
+
underlying files change.
|
|
11
|
+
|
|
12
|
+
GH-344: The same lifespan also creates a :class:`ClientRootsManager`
|
|
13
|
+
that fetches and caches the client-declared directory roots via
|
|
14
|
+
``roots/list`` on session initialisation and refreshes the cache on
|
|
15
|
+
``notifications/roots/list_changed``. The roots are used to scope
|
|
16
|
+
CWD/worktree operations, complementing the GH-979 effective-CWD
|
|
17
|
+
discipline.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import logging
|
|
24
|
+
from collections.abc import AsyncIterator
|
|
25
|
+
from contextlib import asynccontextmanager
|
|
26
|
+
|
|
27
|
+
from mcp.server.fastmcp import FastMCP
|
|
28
|
+
|
|
29
|
+
from dev10x.mcp.resource_watcher import KnowledgeResourceWatcher, wire_watcher_to_server
|
|
30
|
+
from dev10x.mcp.roots_manager import ClientRootsManager, wire_roots_to_server
|
|
31
|
+
from dev10x.subprocess_utils import get_plugin_root
|
|
32
|
+
|
|
33
|
+
log = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@asynccontextmanager
|
|
37
|
+
async def _server_lifespan(app: FastMCP) -> AsyncIterator[None]:
|
|
38
|
+
"""Lifespan that wires the knowledge-resource watcher and roots manager.
|
|
39
|
+
|
|
40
|
+
On server start:
|
|
41
|
+
|
|
42
|
+
1. Creates a :class:`~dev10x.mcp.resource_watcher.KnowledgeResourceWatcher`
|
|
43
|
+
rooted at the plugin install directory and registers its
|
|
44
|
+
``InitializedNotification`` handler (GH-341).
|
|
45
|
+
2. Creates a :class:`~dev10x.mcp.roots_manager.ClientRootsManager` and
|
|
46
|
+
registers its ``InitializedNotification`` handler (chained after the
|
|
47
|
+
watcher's) plus a ``RootsListChangedNotification`` handler (GH-344).
|
|
48
|
+
3. Starts the resource-watcher poll loop as a background asyncio task.
|
|
49
|
+
|
|
50
|
+
On server shutdown (lifespan exit) the background task is cancelled.
|
|
51
|
+
"""
|
|
52
|
+
plugin_root = get_plugin_root()
|
|
53
|
+
watcher = KnowledgeResourceWatcher(plugin_root=plugin_root)
|
|
54
|
+
|
|
55
|
+
# GH-341: resource watcher registers the first InitializedNotification handler.
|
|
56
|
+
wire_watcher_to_server(server=app._mcp_server, watcher=watcher)
|
|
57
|
+
|
|
58
|
+
# GH-344: roots manager chains its handler after the watcher's.
|
|
59
|
+
roots_manager = ClientRootsManager()
|
|
60
|
+
wire_roots_to_server(server=app._mcp_server, manager=roots_manager)
|
|
61
|
+
|
|
62
|
+
task = asyncio.create_task(watcher.run(), name="dev10x-resource-watcher")
|
|
63
|
+
log.debug("Resource watcher task started for root: %s", plugin_root)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
yield
|
|
67
|
+
finally:
|
|
68
|
+
task.cancel()
|
|
69
|
+
try:
|
|
70
|
+
await task
|
|
71
|
+
except asyncio.CancelledError:
|
|
72
|
+
pass
|
|
73
|
+
log.debug("Resource watcher task stopped")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
server = FastMCP(name="Dev10x-cli", lifespan=_server_lifespan)
|