coding-os 0.3.2__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.
- adapters/claude/README.md +6 -0
- adapters/claude/_install_helpers/extract_stacks.py +43 -0
- adapters/claude/_install_helpers/update_mcp_json.py +75 -0
- adapters/claude/adapter.yaml +164 -0
- adapters/claude/hooks/README.md +40 -0
- adapters/claude/hooks/agent_memory_sync.py +131 -0
- adapters/claude/hooks/ensure-agent-memory-link.sh +36 -0
- adapters/claude/hooks/sync-agent-memory.sh +21 -0
- adapters/claude/install.sh +86 -0
- adapters/claude/sdk_dispatcher.py +871 -0
- adapters/claude/settings.local.template.json +31 -0
- adapters/claude/settings.template.json +808 -0
- adapters/claude/update_mcp_json.py +85 -0
- adapters/codex/adapter.yaml +254 -0
- adapters/codex/chat_provider.py +230 -0
- adapters/codex/commands/formula-f1.md +129 -0
- adapters/codex/commands/formula-f10.md +100 -0
- adapters/codex/commands/formula-f11.md +123 -0
- adapters/codex/commands/formula-f2.md +139 -0
- adapters/codex/commands/formula-f3.md +127 -0
- adapters/codex/commands/formula-f4.md +101 -0
- adapters/codex/commands/formula-f5.md +135 -0
- adapters/codex/commands/formula-f6.md +147 -0
- adapters/codex/commands/formula-f7.md +111 -0
- adapters/codex/commands/formula-f8.md +133 -0
- adapters/codex/commands/formula-f9.md +112 -0
- adapters/codex/enable_codex_hooks.py +94 -0
- adapters/codex/ensure_codex_mcp.py +124 -0
- adapters/codex/hooks/codex-merge-hook-output.py +72 -0
- adapters/codex/hooks/codex-normalize-edit.py +96 -0
- adapters/codex/hooks/codex-postedit-dispatch.sh +75 -0
- adapters/codex/hooks/codex-posttool-dispatch.sh +70 -0
- adapters/codex/hooks/codex-preedit-dispatch.sh +83 -0
- adapters/codex/hooks/codex-pretool-dispatch.sh +82 -0
- adapters/codex/hooks/codex-sessionend-dispatch.sh +20 -0
- adapters/codex/hooks/codex-sessionstart-dispatch.sh +68 -0
- adapters/codex/hooks/codex-stop-dispatch.sh +73 -0
- adapters/codex/hooks/codex-userpromptsubmit-dispatch.sh +74 -0
- adapters/codex/hooks.template.json +208 -0
- adapters/codex/install.sh +83 -0
- adapters/codex/sdk_dispatcher.py +449 -0
- board_os/__init__.py +39 -0
- board_os/_agent_runtime.py +256 -0
- board_os/config.py +421 -0
- board_os/git_coherence.py +107 -0
- board_os/hub_adapter_manifest.py +140 -0
- board_os/mcp_tools.py +3228 -0
- board_os/migration.py +166 -0
- board_os/parser.py +317 -0
- board_os/presence.py +156 -0
- board_os/sync.py +320 -0
- board_os/transition_gates.py +224 -0
- board_os/transition_gates_cli.py +272 -0
- board_os/transition_gates_validator.py +551 -0
- board_os/verify_suites.py +126 -0
- board_os/verify_suites_cli.py +328 -0
- board_os/workflow.py +967 -0
- cli/__init__.py +0 -0
- cli/_data_types.py +248 -0
- cli/_init_helpers.py +587 -0
- cli/_resources.py +100 -0
- cli/adapter_registry.py +239 -0
- cli/add_stack.py +315 -0
- cli/aggregator.py +438 -0
- cli/board_commands.py +1218 -0
- cli/brain_commands.py +255 -0
- cli/cognition.py +345 -0
- cli/config_composer.py +349 -0
- cli/core_version.py +41 -0
- cli/cron_commands.py +278 -0
- cli/db_reset.py +298 -0
- cli/doc_commands.py +111 -0
- cli/doctor.py +2953 -0
- cli/doctor_board.py +365 -0
- cli/doctor_extras.py +1121 -0
- cli/doctor_graph.py +608 -0
- cli/doctor_tokens.py +254 -0
- cli/graph_commands.py +1265 -0
- cli/hook_renderer.py +393 -0
- cli/hub_commands.py +580 -0
- cli/list_adapters.py +79 -0
- cli/list_stacks.py +105 -0
- cli/logs_commands.py +89 -0
- cli/main.py +3070 -0
- cli/materialize_file.py +65 -0
- cli/mcp_start.py +153 -0
- cli/module_commands.py +513 -0
- cli/pr_commands.py +2024 -0
- cli/preset_commands.py +126 -0
- cli/preset_registry.py +171 -0
- cli/project_overrides.py +119 -0
- cli/registry.py +365 -0
- cli/remove_stack.py +492 -0
- cli/renderer.py +620 -0
- cli/setup.py +464 -0
- cli/skill_commands.py +689 -0
- cli/skill_registry.py +235 -0
- cli/skills_list.py +332 -0
- cli/stack_lint.py +351 -0
- cli/stack_registry.py +688 -0
- cli/subsystems.py +335 -0
- cli/sync_all.py +310 -0
- cli/tail_command.py +410 -0
- cli/update.py +608 -0
- cli/verify_since_edit.py +439 -0
- coding_os-0.3.2.dist-info/METADATA +508 -0
- coding_os-0.3.2.dist-info/RECORD +1304 -0
- coding_os-0.3.2.dist-info/WHEEL +5 -0
- coding_os-0.3.2.dist-info/entry_points.txt +5 -0
- coding_os-0.3.2.dist-info/licenses/LICENSE +201 -0
- coding_os-0.3.2.dist-info/top_level.txt +10 -0
- core/__init__.py +0 -0
- core/board_os/__init__.py +39 -0
- core/board_os/_agent_runtime.py +256 -0
- core/board_os/config.py +421 -0
- core/board_os/git_coherence.py +107 -0
- core/board_os/hub_adapter_manifest.py +140 -0
- core/board_os/mcp_tools.py +3228 -0
- core/board_os/migration.py +166 -0
- core/board_os/parser.py +317 -0
- core/board_os/presence.py +156 -0
- core/board_os/sync.py +320 -0
- core/board_os/transition-gates.yaml +176 -0
- core/board_os/transition_gates.py +224 -0
- core/board_os/transition_gates_cli.py +272 -0
- core/board_os/transition_gates_validator.py +551 -0
- core/board_os/verify-suites.yaml +113 -0
- core/board_os/verify_suites.py +126 -0
- core/board_os/verify_suites_cli.py +328 -0
- core/board_os/workflow.py +967 -0
- core/commands/board.md +27 -0
- core/commands/classify.md +23 -0
- core/commands/compose.md +23 -0
- core/commands/daily.md +31 -0
- core/commands/diagnose.md +7 -0
- core/commands/memory-search.md +23 -0
- core/commands/new-project.md +33 -0
- core/commands/retro.md +38 -0
- core/commands/review.md +14 -0
- core/commands/task.md +17 -0
- core/commands/verify.md +34 -0
- core/docs/thinking_os-final-edition.md +1449 -0
- core/doctor-config.yaml +74 -0
- core/graph_os/__init__.py +29 -0
- core/graph_os/backend.py +233 -0
- core/graph_os/backends/__init__.py +13 -0
- core/graph_os/backends/sqlite_backend.py +1053 -0
- core/graph_os/bench/__init__.py +17 -0
- core/graph_os/bench/fixtures.py +56 -0
- core/graph_os/bench/harness.py +95 -0
- core/graph_os/bench/persian_precision.py +142 -0
- core/graph_os/bench/scale_500k.py +120 -0
- core/graph_os/bench/token_cost.py +170 -0
- core/graph_os/bench/viewer_fps.py +110 -0
- core/graph_os/communities.py +410 -0
- core/graph_os/enterprise.py +218 -0
- core/graph_os/entry_points.py +226 -0
- core/graph_os/extractors/__init__.py +25 -0
- core/graph_os/extractors/code_generic.py +914 -0
- core/graph_os/extractors/code_go.py +1422 -0
- core/graph_os/extractors/code_json.py +340 -0
- core/graph_os/extractors/code_php.py +979 -0
- core/graph_os/extractors/code_python.py +1454 -0
- core/graph_os/extractors/code_shell.py +538 -0
- core/graph_os/extractors/code_toml.py +302 -0
- core/graph_os/extractors/code_ts.py +1665 -0
- core/graph_os/extractors/code_yaml.py +394 -0
- core/graph_os/extractors/contracts.py +1592 -0
- core/graph_os/extractors/md_links.py +890 -0
- core/graph_os/extractors/task_deps.py +345 -0
- core/graph_os/groups/__init__.py +22 -0
- core/graph_os/groups/cross_repo.py +156 -0
- core/graph_os/groups/manifest.py +141 -0
- core/graph_os/ingest/__init__.py +19 -0
- core/graph_os/ingest/base.py +306 -0
- core/graph_os/ingest/github.py +112 -0
- core/graph_os/ingest/zip.py +95 -0
- core/graph_os/toolchain.py +393 -0
- core/graph_os/tools/__init__.py +9 -0
- core/graph_os/tools/graph.py +5573 -0
- core/graph_os/tools/reindex_dispatch.py +730 -0
- core/graph_os/tree_sitter_overlay.py +235 -0
- core/graph_os/types.py +252 -0
- core/graph_os/vec_index.py +277 -0
- core/graph_os/viewer/__init__.py +12 -0
- core/graph_os/viewer/exporter.py +93 -0
- core/graph_os/viewer/template.py +189 -0
- core/hooks/_helpers/_paths.py +40 -0
- core/hooks/_helpers/advance_role.py +72 -0
- core/hooks/_helpers/auto_compose.py +228 -0
- core/hooks/_helpers/auto_validate_lessons.py +55 -0
- core/hooks/_helpers/branch_guard_check.py +796 -0
- core/hooks/_helpers/check_commit_message.py +108 -0
- core/hooks/_helpers/check_dangerous_rm.py +80 -0
- core/hooks/_helpers/check_git_bypass.py +154 -0
- core/hooks/_helpers/check_git_destructive.py +77 -0
- core/hooks/_helpers/check_settings_write.py +97 -0
- core/hooks/_helpers/consume_override.py +51 -0
- core/hooks/_helpers/context_budget.py +77 -0
- core/hooks/_helpers/cos_say_json.py +103 -0
- core/hooks/_helpers/destructive_edit_check.py +163 -0
- core/hooks/_helpers/detect_status_transition.py +82 -0
- core/hooks/_helpers/digest_regen.py +56 -0
- core/hooks/_helpers/doc_sync_check.py +498 -0
- core/hooks/_helpers/drain_embedding_outbox.py +52 -0
- core/hooks/_helpers/extract_additional_context.py +51 -0
- core/hooks/_helpers/extract_commit_msg_arg.py +74 -0
- core/hooks/_helpers/git_command_parse.py +424 -0
- core/hooks/_helpers/git_settings_fields.py +47 -0
- core/hooks/_helpers/graph_context_match.py +37 -0
- core/hooks/_helpers/graph_marker_check.py +70 -0
- core/hooks/_helpers/jit_recall.py +56 -0
- core/hooks/_helpers/json_field.py +41 -0
- core/hooks/_helpers/narrative_signal.py +59 -0
- core/hooks/_helpers/observation_count.py +31 -0
- core/hooks/_helpers/pre_commit_batch.py +177 -0
- core/hooks/_helpers/pre_commit_fake_input.py +42 -0
- core/hooks/_helpers/presence_gc.py +102 -0
- core/hooks/_helpers/presence_write.py +167 -0
- core/hooks/_helpers/recover_indirect.py +35 -0
- core/hooks/_helpers/routing_evolution.py +104 -0
- core/hooks/_helpers/session_recap.py +72 -0
- core/hooks/_helpers/skill_primer.py +229 -0
- core/hooks/_helpers/task_sync.py +59 -0
- core/hooks/_helpers/tool_failure_capture.py +147 -0
- core/hooks/_helpers/trajectory_autosnap.py +278 -0
- core/hooks/_helpers/trajectory_startup.py +62 -0
- core/hooks/_helpers/turn_summary.py +82 -0
- core/hooks/_helpers/validate_task_frontmatter.py +98 -0
- core/hooks/_helpers/wip_limit_check.py +103 -0
- core/hooks/_helpers/wip_lines.py +53 -0
- core/hooks/_helpers/work_log_append.py +89 -0
- core/hooks/_helpers/wrap_dispatch_output.py +82 -0
- core/hooks/advance-role.sh +48 -0
- core/hooks/agent-presence.sh +179 -0
- core/hooks/auto-brain-decay.sh +184 -0
- core/hooks/auto-compose-roles.sh +83 -0
- core/hooks/auto-graph-reconcile-shell.sh +119 -0
- core/hooks/auto-regen-doc-index.sh +120 -0
- core/hooks/auto-reindex-docs.sh +130 -0
- core/hooks/auto-task-sync.sh +56 -0
- core/hooks/auto-trace-rotate.sh +88 -0
- core/hooks/block-bad-patterns.sh +212 -0
- core/hooks/block-dangerous-commands.sh +182 -0
- core/hooks/block-hardcoded-literals.sh +90 -0
- core/hooks/block-migration-conflict.sh +114 -0
- core/hooks/block-protected-files.sh +129 -0
- core/hooks/block-secrets.sh +185 -0
- core/hooks/block-shared-tree-edit.sh +75 -0
- core/hooks/block-uv-heredoc.sh +78 -0
- core/hooks/branch-guard.sh +122 -0
- core/hooks/capture-observation.sh +76 -0
- core/hooks/capture-tool-failure.sh +24 -0
- core/hooks/capture-work-log.sh +89 -0
- core/hooks/check-agents-md-refs.sh +75 -0
- core/hooks/check-agents-md-size.sh +49 -0
- core/hooks/check-capture-worked.sh +148 -0
- core/hooks/check-doc-size.sh +61 -0
- core/hooks/check-mcp-extras.sh +92 -0
- core/hooks/check-state.sh +87 -0
- core/hooks/classify-task-mode.sh +103 -0
- core/hooks/cos-env.sh +1301 -0
- core/hooks/drain-embedding-outbox.sh +24 -0
- core/hooks/enforce-anti-ambiguity.sh +74 -0
- core/hooks/enforce-commit-message.sh +73 -0
- core/hooks/enforce-doc-anchor.sh +224 -0
- core/hooks/enforce-doc-sync.sh +206 -0
- core/hooks/enforce-graph-context.sh +89 -0
- core/hooks/enforce-graph-first-read.sh +94 -0
- core/hooks/enforce-memory-check.sh +128 -0
- core/hooks/enforce-rename-plan.sh +78 -0
- core/hooks/enforce-scaffold-boundary.sh +68 -0
- core/hooks/enforce-skill.sh +125 -0
- core/hooks/enforce-task-body.sh +52 -0
- core/hooks/enforce-task-start.sh +81 -0
- core/hooks/enforce-task-transition.sh +75 -0
- core/hooks/enforce-template.sh +143 -0
- core/hooks/enforce-verify.sh +112 -0
- core/hooks/enforce-wip-limit.sh +43 -0
- core/hooks/enforce-zoom.sh +69 -0
- core/hooks/ensure-hub-up.sh +67 -0
- core/hooks/inject-mcp-caller-session.sh +70 -0
- core/hooks/jit-recall.sh +65 -0
- core/hooks/link-commit-to-task.sh +143 -0
- core/hooks/lint-task.sh +40 -0
- core/hooks/nudge-docs-first.sh +71 -0
- core/hooks/nudge-git-mode.sh +29 -0
- core/hooks/nudge-graph-os.sh +118 -0
- core/hooks/nudge-learn-narrative.sh +36 -0
- core/hooks/nudge-model-routing.sh +32 -0
- core/hooks/nudge-reentry.sh +101 -0
- core/hooks/nudge-reuse-first.sh +68 -0
- core/hooks/nudge-task-discovery.sh +81 -0
- core/hooks/nudge-thinking-os.sh +109 -0
- core/hooks/pr-reap.sh +23 -0
- core/hooks/reclaim-sweep.sh +58 -0
- core/hooks/record-verify-auto.sh +77 -0
- core/hooks/record-verify.sh +74 -0
- core/hooks/regen-reminder.sh +104 -0
- core/hooks/registry.yaml +1262 -0
- core/hooks/remind-daily.sh +27 -0
- core/hooks/remind-dogfood.sh +70 -0
- core/hooks/remind-learn-validate.sh +94 -0
- core/hooks/rules-primer.sh +50 -0
- core/hooks/search-enforce-inventory.sh +108 -0
- core/hooks/search-verify-remaining.sh +132 -0
- core/hooks/session-context.sh +729 -0
- core/hooks/session-end.sh +145 -0
- core/hooks/session-skill-primer.sh +43 -0
- core/hooks/snapshot-transcript.sh +56 -0
- core/hooks/sync-task-current.sh +85 -0
- core/hooks/test-first-reminder.sh +120 -0
- core/hooks/test-governor.sh +173 -0
- core/hooks/thinking_os-gate.sh +52 -0
- core/hooks/track-backtrack.sh +35 -0
- core/hooks/track-discovery.sh +121 -0
- core/hooks/track-skill.sh +53 -0
- core/hooks/validate-task-frontmatter.sh +49 -0
- core/hooks/verify-rename-callers.sh +119 -0
- core/hooks/warn-abandoned-task.sh +99 -0
- core/hooks/warn-destructive-edit.sh +64 -0
- core/hooks/warn-diff-size.sh +43 -0
- core/hooks/warn-graph-empty.sh +81 -0
- core/hooks/warn-mcp-down.sh +190 -0
- core/hooks/write-state.sh +55 -0
- core/logging_os/__init__.py +33 -0
- core/logging_os/api.py +127 -0
- core/logging_os/bridge.py +80 -0
- core/logging_os/config.py +172 -0
- core/logging_os/fingerprint.py +25 -0
- core/logging_os/redact.py +53 -0
- core/logging_os/render.py +83 -0
- core/logging_os/sinks.py +164 -0
- core/rules/anti-overengineering.md +44 -0
- core/rules/api-contract-discipline.md +41 -0
- core/rules/dimension-registry.md +155 -0
- core/rules/git-workflow.md +57 -0
- core/rules/memory.md +46 -0
- core/rules/model-routing.md +22 -0
- core/rules/skill-enforcement.md +75 -0
- core/rules/test-discipline.md +38 -0
- core/rules/thinking_os.md +48 -0
- core/rules/transparency-banner.md +37 -0
- core/runtime_paths.yaml +36 -0
- core/scaffold_manifest.json +14430 -0
- core/scheduled/__init__.py +0 -0
- core/scheduled/_activity.py +126 -0
- core/scheduled/_state.py +113 -0
- core/scheduled/config.py +86 -0
- core/scheduled/dep_reconcile.py +135 -0
- core/scheduled/error_sweep.py +137 -0
- core/scheduled/nightly.py +930 -0
- core/scheduled/responsive_extract.py +65 -0
- core/schemas/adapter.schema.json +269 -0
- core/schemas/preset.schema.json +50 -0
- core/schemas/skill.schema.json +81 -0
- core/schemas/stack.schema.json +404 -0
- core/scripts/_lib.sh +9 -0
- core/scripts/docs-lint.sh +228 -0
- core/scripts/docs-nav-fix.sh +133 -0
- core/scripts/docs-staleness-check.sh +154 -0
- core/scripts/install-adapter.sh +266 -0
- core/scripts/link-stack-skills.sh +52 -0
- core/scripts/log-latest.sh +106 -0
- core/scripts/log-search.sh +89 -0
- core/scripts/log-write.sh +134 -0
- core/scripts/ref-resolve.sh +71 -0
- core/skills/a11y/SKILL.md +305 -0
- core/skills/a11y/assets/a11y-checklist.md +137 -0
- core/skills/a11y/references/aria-and-focus.md +247 -0
- core/skills/a11y/references/rn-accessibility.md +343 -0
- core/skills/a11y/references/screen-reader-testing.md +190 -0
- core/skills/agent-memory/SKILL.md +191 -0
- core/skills/agent-memory/assets/memory-checklist.md +21 -0
- core/skills/agent-memory/references/memory-recipes.md +57 -0
- core/skills/api-design/SKILL.md +232 -0
- core/skills/api-design/assets/api-design-checklist.md +110 -0
- core/skills/api-design/references/error-envelope.md +381 -0
- core/skills/api-design/references/idempotency-pagination.md +312 -0
- core/skills/api-design/references/rest-contracts.md +426 -0
- core/skills/auth-patterns/SKILL.md +352 -0
- core/skills/auth-patterns/assets/auth-checklist.md +118 -0
- core/skills/auth-patterns/references/jwt-and-service-tokens.md +343 -0
- core/skills/auth-patterns/references/passkeys-2fa.md +289 -0
- core/skills/auth-patterns/references/sessions-vs-jwt.md +230 -0
- core/skills/auth-patterns/scripts/cookie-flag-check.py +146 -0
- core/skills/backend-fundamentals/SKILL.md +238 -0
- core/skills/backend-fundamentals/assets/backend-checklist.md +27 -0
- core/skills/backend-fundamentals/references/backend-patterns.md +56 -0
- core/skills/backend-fundamentals/scripts/check_layering.py +83 -0
- core/skills/clean-code/SKILL.md +642 -0
- core/skills/clean-code/scripts/audit-fail-closed.py +167 -0
- core/skills/codebase-explorer/SKILL.md +89 -0
- core/skills/codebase-explorer/assets/reading-checklist.md +24 -0
- core/skills/codebase-explorer/references/reading-strategies.md +52 -0
- core/skills/codebase-explorer/scripts/outline.py +99 -0
- core/skills/db-design/SKILL.md +327 -0
- core/skills/db-design/assets/migration-template.sql +49 -0
- core/skills/db-design/references/migration-discipline.md +290 -0
- core/skills/db-design/references/postgres-patterns.md +340 -0
- core/skills/db-design/scripts/migration-safety.sh +150 -0
- core/skills/deployment-cicd/SKILL.md +260 -0
- core/skills/deployment-cicd/assets/deploy-checklist.md +26 -0
- core/skills/deployment-cicd/references/pipeline-and-release.md +54 -0
- core/skills/deployment-cicd/scripts/lint_workflow.py +79 -0
- core/skills/docker/SKILL.md +114 -0
- core/skills/docker/assets/dockerfile-checklist.md +31 -0
- core/skills/docker/references/compose-patterns.md +66 -0
- core/skills/docker/references/dockerfile-optimization.md +64 -0
- core/skills/docker/scripts/lint_dockerfile.sh +48 -0
- core/skills/docker/versions.json +16 -0
- core/skills/end-to-end-testing/SKILL.md +101 -0
- core/skills/end-to-end-testing/assets/e2e-checklist.md +23 -0
- core/skills/end-to-end-testing/references/maestro.md +63 -0
- core/skills/end-to-end-testing/references/playwright.md +68 -0
- core/skills/end-to-end-testing/scripts/lint_e2e.py +88 -0
- core/skills/end-to-end-testing/versions.json +16 -0
- core/skills/frontend-design/SKILL.md +76 -0
- core/skills/frontend-design/assets/design-checklist.md +29 -0
- core/skills/frontend-design/references/design-principles.md +65 -0
- core/skills/frontend-design/scripts/check_contrast.py +89 -0
- core/skills/frontend-fundamentals/SKILL.md +213 -0
- core/skills/frontend-fundamentals/assets/frontend-checklist.md +25 -0
- core/skills/frontend-fundamentals/references/rendering-and-state.md +66 -0
- core/skills/frontend-fundamentals/scripts/check_frontend.py +86 -0
- core/skills/graph-explorer/SKILL.md +215 -0
- core/skills/graph-explorer/scripts/explain-impact.sh +64 -0
- core/skills/graphql/SKILL.md +187 -0
- core/skills/grpc-microservices/SKILL.md +174 -0
- core/skills/hexagonal-architecture/SKILL.md +199 -0
- core/skills/hexagonal-architecture/assets/folder-scaffold.md +233 -0
- core/skills/hexagonal-architecture/references/anti-patterns.md +129 -0
- core/skills/hexagonal-architecture/references/go-fiber-layout.md +429 -0
- core/skills/hexagonal-architecture/references/python-fastapi-layout.md +453 -0
- core/skills/hexagonal-architecture/references/react-native-layout.md +428 -0
- core/skills/i18n/SKILL.md +126 -0
- core/skills/incident-response/SKILL.md +225 -0
- core/skills/incident-response/assets/incident-checklist.md +29 -0
- core/skills/incident-response/references/severity-and-runbook.md +53 -0
- core/skills/incident-response/scripts/classify_severity.py +86 -0
- core/skills/linux-sysadmin/SKILL.md +115 -0
- core/skills/linux-sysadmin/assets/hardening-checklist.md +29 -0
- core/skills/linux-sysadmin/references/ssh-hardening.md +62 -0
- core/skills/linux-sysadmin/references/systemd-and-services.md +77 -0
- core/skills/linux-sysadmin/scripts/triage.sh +50 -0
- core/skills/linux-sysadmin/versions.json +17 -0
- core/skills/llm-patterns/SKILL.md +410 -0
- core/skills/llm-patterns/assets/llm-feature-checklist.md +26 -0
- core/skills/llm-patterns/references/rag-and-evals.md +59 -0
- core/skills/llm-patterns/scripts/estimate_tokens.py +75 -0
- core/skills/messaging-queues/SKILL.md +142 -0
- core/skills/mobile-fundamentals/SKILL.md +406 -0
- core/skills/mobile-fundamentals/assets/mobile-launch-checklist.md +130 -0
- core/skills/mobile-fundamentals/references/navigation-and-deep-links.md +337 -0
- core/skills/mobile-fundamentals/references/offline-sync.md +339 -0
- core/skills/node-backend/SKILL.md +114 -0
- core/skills/node-backend/assets/node-checklist.md +25 -0
- core/skills/node-backend/references/async-and-errors.md +67 -0
- core/skills/node-backend/references/event-loop.md +65 -0
- core/skills/node-backend/scripts/check_package.py +78 -0
- core/skills/node-backend/versions.json +17 -0
- core/skills/observability/SKILL.md +289 -0
- core/skills/observability/assets/observability-checklist.md +27 -0
- core/skills/observability/references/instrumentation.md +57 -0
- core/skills/observability/scripts/lint_logging.py +77 -0
- core/skills/payments/SKILL.md +102 -0
- core/skills/performance/SKILL.md +305 -0
- core/skills/performance/assets/perf-checklist.md +143 -0
- core/skills/performance/references/mobile-performance.md +249 -0
- core/skills/performance/references/web-vitals.md +209 -0
- core/skills/php/SKILL.md +116 -0
- core/skills/php/assets/php-checklist.md +26 -0
- core/skills/php/references/modern-php.md +64 -0
- core/skills/php/references/security.md +72 -0
- core/skills/php/scripts/scan_php_smells.py +99 -0
- core/skills/php/versions.json +9 -0
- core/skills/pr-mode-driver/SKILL.md +65 -0
- core/skills/realtime-websockets/SKILL.md +152 -0
- core/skills/redis/SKILL.md +105 -0
- core/skills/redis/assets/redis-checklist.md +27 -0
- core/skills/redis/references/operations.md +66 -0
- core/skills/redis/references/patterns.md +62 -0
- core/skills/redis/scripts/analyze_info.py +101 -0
- core/skills/redis/versions.json +9 -0
- core/skills/search/SKILL.md +91 -0
- core/skills/search/references/grep.md +76 -0
- core/skills/search/scripts/verify-count.sh +73 -0
- core/skills/search-infra/SKILL.md +109 -0
- core/skills/security-mobile/SKILL.md +394 -0
- core/skills/security-mobile/assets/mobile-security-checklist.md +117 -0
- core/skills/security-mobile/references/masvs-l1-checklist.md +127 -0
- core/skills/security-web/SKILL.md +217 -0
- core/skills/security-web/assets/security-web-checklist.md +167 -0
- core/skills/security-web/references/owasp-top-10.md +551 -0
- core/skills/security-web/references/supply-chain.md +179 -0
- core/skills/security-web/scripts/csp-check.sh +153 -0
- core/skills/shell-scripting/SKILL.md +128 -0
- core/skills/shell-scripting/assets/script-checklist.md +33 -0
- core/skills/shell-scripting/references/argument-parsing.md +84 -0
- core/skills/shell-scripting/references/bash-robustness.md +78 -0
- core/skills/shell-scripting/scripts/lint_script.sh +53 -0
- core/skills/shell-scripting/scripts/new_script.py +131 -0
- core/skills/shell-scripting/versions.json +23 -0
- core/skills/sql-authoring/SKILL.md +104 -0
- core/skills/sql-authoring/assets/query-review-checklist.md +26 -0
- core/skills/sql-authoring/references/query-patterns.md +89 -0
- core/skills/sql-authoring/references/reading-explain.md +52 -0
- core/skills/sql-authoring/scripts/analyze_plan.py +100 -0
- core/skills/sql-authoring/versions.json +17 -0
- core/skills/state-management/SKILL.md +428 -0
- core/skills/state-management/references/tanstack-query-recipes.md +301 -0
- core/skills/state-management/references/zustand-recipes.md +364 -0
- core/skills/supabase/SKILL.md +108 -0
- core/skills/supabase/assets/supabase-checklist.md +26 -0
- core/skills/supabase/references/realtime-and-storage.md +55 -0
- core/skills/supabase/references/rls-and-auth.md +65 -0
- core/skills/supabase/scripts/check_rls.py +88 -0
- core/skills/supabase/versions.json +9 -0
- core/skills/task-driver/SKILL.md +272 -0
- core/skills/task-driver/scripts/task-lint.sh +163 -0
- core/skills/technical-writing/SKILL.md +85 -0
- core/skills/technical-writing/assets/doc-checklist.md +29 -0
- core/skills/technical-writing/references/doc-anatomy.md +53 -0
- core/skills/technical-writing/references/writing-craft.md +59 -0
- core/skills/technical-writing/scripts/new_doc.py +81 -0
- core/skills/terraform-k8s/SKILL.md +133 -0
- core/skills/testing-strategy/SKILL.md +266 -0
- core/skills/testing-strategy/assets/test-review-checklist.md +25 -0
- core/skills/testing-strategy/references/test-types.md +52 -0
- core/skills/testing-strategy/scripts/coverage_gate.py +79 -0
- core/skills/thinking_os/SKILL.md +288 -0
- core/skills/thinking_os/scripts/classify.sh +122 -0
- core/skills/typescript/SKILL.md +110 -0
- core/skills/typescript/assets/typescript-checklist.md +24 -0
- core/skills/typescript/references/strictness.md +53 -0
- core/skills/typescript/references/type-system.md +86 -0
- core/skills/typescript/scripts/check_tsconfig.py +92 -0
- core/skills/typescript/versions.json +9 -0
- core/subsystems.yaml +202 -0
- core/thinking_os/__init__.py +1 -0
- core/thinking_os/_agent_markers.py +32 -0
- core/thinking_os/agents/README.md +71 -0
- core/thinking_os/agents/analyst.md +139 -0
- core/thinking_os/agents/architect.md +127 -0
- core/thinking_os/agents/debugger.md +111 -0
- core/thinking_os/agents/deployer.md +112 -0
- core/thinking_os/agents/distiller.md +28 -0
- core/thinking_os/agents/documenter.md +101 -0
- core/thinking_os/agents/implementer.md +135 -0
- core/thinking_os/agents/internal/session_observer.md +39 -0
- core/thinking_os/agents/observer.md +100 -0
- core/thinking_os/agents/onboarder.md +81 -0
- core/thinking_os/agents/refactorer.md +123 -0
- core/thinking_os/agents/repairer.md +47 -0
- core/thinking_os/agents/researcher.md +129 -0
- core/thinking_os/agents/reviewer.md +147 -0
- core/thinking_os/agents/security_auditor.md +133 -0
- core/thinking_os/background.py +405 -0
- core/thinking_os/bootstrap_outcomes.py +200 -0
- core/thinking_os/budget.py +302 -0
- core/thinking_os/capture.py +495 -0
- core/thinking_os/cognition.py +516 -0
- core/thinking_os/cognition_schemas.py +517 -0
- core/thinking_os/compress.py +192 -0
- core/thinking_os/concepts.py +233 -0
- core/thinking_os/dashboard.py +159 -0
- core/thinking_os/database.py +2883 -0
- core/thinking_os/decay.py +393 -0
- core/thinking_os/digest.py +295 -0
- core/thinking_os/dispatcher.py +192 -0
- core/thinking_os/dispatcher_helpers.py +48 -0
- core/thinking_os/dispatchers/__init__.py +3 -0
- core/thinking_os/dispatchers/default.py +47 -0
- core/thinking_os/distill.py +192 -0
- core/thinking_os/doc_indexer.py +905 -0
- core/thinking_os/embeddings.py +943 -0
- core/thinking_os/formula_composer.py +556 -0
- core/thinking_os/gate_marker.py +75 -0
- core/thinking_os/graph.py +296 -0
- core/thinking_os/graph_indexer.py +360 -0
- core/thinking_os/health_check.py +517 -0
- core/thinking_os/impact.py +119 -0
- core/thinking_os/memory_gc.py +356 -0
- core/thinking_os/migrator_embeddings.py +318 -0
- core/thinking_os/precision.py +194 -0
- core/thinking_os/presets/registry.yaml +127 -0
- core/thinking_os/record_outcome.py +399 -0
- core/thinking_os/repair.py +105 -0
- core/thinking_os/retrieval_quality.py +239 -0
- core/thinking_os/roles/analyst.yaml +83 -0
- core/thinking_os/roles/architect.yaml +88 -0
- core/thinking_os/roles/debugger.yaml +71 -0
- core/thinking_os/roles/deployer.yaml +71 -0
- core/thinking_os/roles/documenter.yaml +72 -0
- core/thinking_os/roles/implementer.yaml +83 -0
- core/thinking_os/roles/observer.yaml +70 -0
- core/thinking_os/roles/refactorer.yaml +71 -0
- core/thinking_os/roles/researcher.yaml +72 -0
- core/thinking_os/roles/reviewer.yaml +79 -0
- core/thinking_os/roles/security_auditor.yaml +83 -0
- core/thinking_os/roles_state.py +168 -0
- core/thinking_os/sanitizer.py +320 -0
- core/thinking_os/server.py +3160 -0
- core/thinking_os/session_enrich.py +272 -0
- core/thinking_os/session_observe_worker.py +111 -0
- core/thinking_os/session_startup.py +74 -0
- core/thinking_os/session_summary.py +245 -0
- core/thinking_os/situations/registry.yaml +99 -0
- core/thinking_os/task_analyzer.py +462 -0
- core/thinking_os/task_parser.py +342 -0
- core/thinking_os/task_sync.py +73 -0
- core/thinking_os/tools/__init__.py +6 -0
- core/thinking_os/tools/_shared.py +947 -0
- core/thinking_os/tools/cognition.py +1867 -0
- core/thinking_os/tools/docs.py +770 -0
- core/thinking_os/tools/learning.py +2078 -0
- core/thinking_os/tools/logs.py +79 -0
- core/thinking_os/tools/memory.py +840 -0
- core/thinking_os/tools/metrics.py +200 -0
- core/thinking_os/tools/retrieve.py +415 -0
- core/thinking_os/tools/routing.py +658 -0
- core/thinking_os/tools/tasks.py +449 -0
- core/thinking_os/tools/trajectory.py +181 -0
- core/thinking_os/tracing.py +235 -0
- core/web/__init__.py +5 -0
- core/web/_cache.py +118 -0
- core/web/_deps.py +56 -0
- core/web/_envelope.py +85 -0
- core/web/_project_context.py +140 -0
- core/web/chat_providers.py +108 -0
- core/web/init_jobs.py +216 -0
- core/web/routes/__init__.py +25 -0
- core/web/routes/_bounded_read.py +76 -0
- core/web/routes/board.py +1089 -0
- core/web/routes/cognition.py +1838 -0
- core/web/routes/config.py +635 -0
- core/web/routes/graph.py +513 -0
- core/web/routes/health.py +180 -0
- core/web/routes/hooks.py +288 -0
- core/web/routes/hub.py +1219 -0
- core/web/routes/logs.py +374 -0
- core/web/routes/metrics.py +43 -0
- core/web/routes/observability.py +400 -0
- core/web/routes/patterns.py +227 -0
- core/web/routes/presence.py +609 -0
- core/web/routes/roles.py +446 -0
- core/web/routes/scheduled.py +261 -0
- core/web/routes/search.py +238 -0
- core/web/routes/sessions.py +220 -0
- core/web/routes/settings.py +363 -0
- core/web/routes/stream.py +547 -0
- core/web/security.py +157 -0
- core/web/server.py +283 -0
- graph_os/__init__.py +29 -0
- graph_os/backend.py +233 -0
- graph_os/backends/__init__.py +13 -0
- graph_os/backends/sqlite_backend.py +1053 -0
- graph_os/communities.py +410 -0
- graph_os/enterprise.py +218 -0
- graph_os/entry_points.py +226 -0
- graph_os/extractors/__init__.py +25 -0
- graph_os/extractors/code_generic.py +914 -0
- graph_os/extractors/code_go.py +1422 -0
- graph_os/extractors/code_json.py +340 -0
- graph_os/extractors/code_php.py +979 -0
- graph_os/extractors/code_python.py +1454 -0
- graph_os/extractors/code_shell.py +538 -0
- graph_os/extractors/code_toml.py +302 -0
- graph_os/extractors/code_ts.py +1665 -0
- graph_os/extractors/code_yaml.py +394 -0
- graph_os/extractors/contracts.py +1592 -0
- graph_os/extractors/md_links.py +890 -0
- graph_os/extractors/task_deps.py +345 -0
- graph_os/groups/__init__.py +22 -0
- graph_os/groups/cross_repo.py +156 -0
- graph_os/groups/manifest.py +141 -0
- graph_os/ingest/__init__.py +19 -0
- graph_os/ingest/base.py +306 -0
- graph_os/ingest/github.py +112 -0
- graph_os/ingest/zip.py +95 -0
- graph_os/toolchain.py +393 -0
- graph_os/tools/__init__.py +9 -0
- graph_os/tools/graph.py +5573 -0
- graph_os/tools/reindex_dispatch.py +730 -0
- graph_os/tree_sitter_overlay.py +235 -0
- graph_os/types.py +252 -0
- graph_os/vec_index.py +277 -0
- graph_os/viewer/__init__.py +12 -0
- graph_os/viewer/exporter.py +93 -0
- graph_os/viewer/template.py +189 -0
- scheduled/__init__.py +0 -0
- scheduled/_activity.py +126 -0
- scheduled/_state.py +113 -0
- scheduled/config.py +86 -0
- scheduled/dep_reconcile.py +135 -0
- scheduled/error_sweep.py +137 -0
- scheduled/nightly.py +930 -0
- scheduled/responsive_extract.py +65 -0
- scripts/__init__.py +4 -0
- scripts/_commit_msg_body.sh +31 -0
- scripts/_post_commit_body.sh +49 -0
- scripts/_pre_commit_body.sh +121 -0
- scripts/_prepare_commit_msg_body.sh +53 -0
- scripts/audit_mcp_tools.py +693 -0
- scripts/bench_sdk_dispatcher.py +177 -0
- scripts/capture_golden.py +169 -0
- scripts/check_graph_phantoms.py +75 -0
- scripts/dev/audit_doc_links.py +359 -0
- scripts/dev/audit_scaffold_module_tags.py +107 -0
- scripts/dev/backfill_doc_headers.py +328 -0
- scripts/dev/backfill_nav_lines.py +119 -0
- scripts/dev/fix_nav_placement.py +106 -0
- scripts/dev/inspect_sdk_options.py +45 -0
- scripts/dev/migrate_check_ids.py +170 -0
- scripts/dev/strip_purpose_blocks.py +154 -0
- scripts/dump_openapi.py +66 -0
- scripts/e2e_dispatch_tool.py +195 -0
- scripts/generate_manifest.py +166 -0
- scripts/golden_sections.py +20 -0
- scripts/graph_demo.py +161 -0
- scripts/install-git-hooks.sh +47 -0
- scripts/migrate_embeddings_minilm_to_bge_m3.py +84 -0
- scripts/operational_eval.py +445 -0
- scripts/probe_agent_session_resolver.py +59 -0
- scripts/prune_deleted_path.py +127 -0
- scripts/refactor_agent_dual_mode.py +171 -0
- scripts/refresh_skill_versions.py +302 -0
- scripts/regen_doc_index.py +209 -0
- scripts/regen_doctor_schema.py +62 -0
- scripts/regen_rules.py +94 -0
- scripts/rename_formulas_to_semantic.py +241 -0
- scripts/smoke_db_connections.py +183 -0
- scripts/smoke_doc_header.py +72 -0
- scripts/smoke_graph_e2e.py +374 -0
- scripts/smoke_sdk_dispatch.py +84 -0
- scripts/smoke_uid_resolver.py +164 -0
- scripts/verify_dispatchers.py +244 -0
- scripts/verify_phase_c_e2e.py +436 -0
- templates/__init__.py +6 -0
- templates/_base/Makefile.base +353 -0
- templates/_base/base.yaml +59 -0
- templates/_base/coding-os.yaml.template +39 -0
- templates/_base/dimension-registry.template.md +68 -0
- templates/_base/domain-config.template.json +46 -0
- templates/_base/fragments/anatomy-map.md.tmpl +11 -0
- templates/_base/fragments/context-discipline.md.tmpl +3 -0
- templates/_base/fragments/core-loop.md.tmpl +52 -0
- templates/_base/fragments/engineering-routing.md.tmpl +3 -0
- templates/_base/fragments/header.md.tmpl +6 -0
- templates/_base/fragments/identity.md.tmpl +3 -0
- templates/_base/fragments/principles.md.tmpl +3 -0
- templates/_base/fragments/retrieval-routing.md.tmpl +23 -0
- templates/_base/fragments/session-handoff.md.tmpl +3 -0
- templates/_base/fragments/skills.md.tmpl +3 -0
- templates/_base/fragments/ssot-map.md.tmpl +3 -0
- templates/_base/fragments/stop-conditions.md.tmpl +3 -0
- templates/_base/fragments/subagent-dispatch.md.tmpl +3 -0
- templates/_base/fragments/task-authoring.md.tmpl +69 -0
- templates/_base/fragments/task-logging.md.tmpl +8 -0
- templates/_base/fragments/tool-routing.md.tmpl +9 -0
- templates/_base/fragments/verification-matrix.md.tmpl +12 -0
- templates/_base/lang/dart/analysis_options.yaml +7 -0
- templates/_base/lang/php/phpcs.xml.dist +10 -0
- templates/_base/lang/python/pyproject.toml +19 -0
- templates/_base/lang/rust/clippy.toml +5 -0
- templates/_base/lang/rust/rustfmt.toml +3 -0
- templates/_base/lang/typescript/eslint.config.js +26 -0
- templates/_base/lang/typescript/tsconfig.json +15 -0
- templates/_base/lang/typescript/vitest.config.ts +10 -0
- templates/_base/scaffold/changes.log +1 -0
- templates/_base/scaffold/docs/00-index.md +55 -0
- templates/_base/scaffold/docs/_meta/feature-dependency-tree.md +30 -0
- templates/_base/scaffold/docs/_meta/foundation-map.md +56 -0
- templates/_base/scaffold/docs/_meta/questions.md +8 -0
- templates/_base/scaffold/docs/_meta/roadmap.md +33 -0
- templates/_base/scaffold/docs/api-contracts/00-index.md +58 -0
- templates/_base/scaffold/docs/api-contracts/error-format.md +58 -0
- templates/_base/scaffold/docs/architecture/00-index.md +42 -0
- templates/_base/scaffold/docs/architecture/adr/00-index.md +39 -0
- templates/_base/scaffold/docs/engineering/00-index.md +9 -0
- templates/_base/scaffold/docs/governance/00-index.md +55 -0
- templates/_base/scaffold/docs/governance/_templates/doc-cheat-sheet.md +202 -0
- templates/_base/scaffold/docs/governance/_templates/playbook-template.md +81 -0
- templates/_base/scaffold/docs/governance/_templates/post-mortem-template.md +85 -0
- templates/_base/scaffold/docs/governance/_templates/runbook-template.md +88 -0
- templates/_base/scaffold/docs/governance/_templates/security-review-template.md +111 -0
- templates/_base/scaffold/docs/governance/_templates/task-detail.md +61 -0
- templates/_base/scaffold/docs/governance/agent-workflow.md +101 -0
- templates/_base/scaffold/docs/governance/anatomy-contract.md +150 -0
- templates/_base/scaffold/docs/governance/critical-rules.md +224 -0
- templates/_base/scaffold/docs/governance/decision-records.md +58 -0
- templates/_base/scaffold/docs/governance/docs-first-protocol.md +157 -0
- templates/_base/scaffold/docs/governance/docs-system.md +151 -0
- templates/_base/scaffold/docs/governance/gdpr-compliance.md +66 -0
- templates/_base/scaffold/docs/governance/mcp-tool-inventory.md +112 -0
- templates/_base/scaffold/docs/governance/risk-register.md +26 -0
- templates/_base/scaffold/docs/governance/scaffold-boundary-contract.md +161 -0
- templates/_base/scaffold/docs/governance/task-lifecycle.md +125 -0
- templates/_base/scaffold/docs/governance/wrapper-derivation.md +50 -0
- templates/_base/scaffold/docs/insights/00-index.md +17 -0
- templates/_base/scaffold/docs/ops/00-index.md +59 -0
- templates/_base/scaffold/docs/ops/runbooks/00-index.md +9 -0
- templates/_base/scaffold/docs/playbooks/00-index.md +12 -0
- templates/_base/scaffold/docs/playbooks/research-validation.md +29 -0
- templates/_base/scaffold/docs/playbooks/security-review.md +41 -0
- templates/_base/scaffold/docs/prd/00-index.md +43 -0
- templates/_base/scaffold/docs/prd/01-snapshot-vision.md +56 -0
- templates/_base/scaffold/docs/workflow/workflow-guide.md +138 -0
- templates/_base/scaffold/src/shared/README.md +23 -0
- templates/_base/skill-enforcement.template.md +14 -0
- templates/_base/task-detail.template.md +61 -0
- templates/_presets/ai-saas.yaml +9 -0
- templates/_presets/django-next.yaml +8 -0
- templates/_presets/dotnet-react.yaml +8 -0
- templates/_presets/flutter-baas.yaml +8 -0
- templates/_presets/go-react.yaml +8 -0
- templates/_presets/hexagonal-product.yaml +16 -0
- templates/_presets/jamstack.yaml +8 -0
- templates/_presets/laravel-vue.yaml +8 -0
- templates/_presets/mean.yaml +8 -0
- templates/_presets/mern.yaml +9 -0
- templates/_presets/nest-angular.yaml +8 -0
- templates/_presets/nextjs-fastapi.yaml +10 -0
- templates/_presets/nuxt-fullstack.yaml +9 -0
- templates/_presets/pern.yaml +9 -0
- templates/_presets/rails-react.yaml +8 -0
- templates/_presets/rn-api.yaml +8 -0
- templates/_presets/rust-svelte.yaml +8 -0
- templates/_presets/spring-react.yaml +8 -0
- templates/_presets/t3-style.yaml +9 -0
- templates/_presets/tall.yaml +8 -0
- templates/_presets/wordpress-cms.yaml +8 -0
- templates/angular/rules/frontend.md +19 -0
- templates/angular/scaffold/docs/engineering/accessibility.md +46 -0
- templates/angular/scaffold/docs/engineering/angular-rules.md +35 -0
- templates/angular/scaffold/docs/playbooks/angular-app.md +42 -0
- templates/angular/scaffold/src/frontend/angular.json +52 -0
- templates/angular/scaffold/src/frontend/package.json +28 -0
- templates/angular/scaffold/src/frontend/src/app/app.component.ts +19 -0
- templates/angular/scaffold/src/frontend/src/app/app.config.ts +22 -0
- templates/angular/scaffold/src/frontend/src/app/app.routes.ts +8 -0
- templates/angular/scaffold/src/frontend/src/app/core/global-error-handler.ts +12 -0
- templates/angular/scaffold/src/frontend/src/app/health/health.component.ts +14 -0
- templates/angular/scaffold/src/frontend/src/app/health/health.service.spec.ts +16 -0
- templates/angular/scaffold/src/frontend/src/app/health/health.service.ts +10 -0
- templates/angular/scaffold/src/frontend/src/index.html +11 -0
- templates/angular/scaffold/src/frontend/src/main.ts +9 -0
- templates/angular/scaffold/src/frontend/src/styles.css +14 -0
- templates/angular/scaffold/src/frontend/tsconfig.app.json +8 -0
- templates/angular/scaffold/src/frontend/tsconfig.json +27 -0
- templates/angular/scaffold/src/frontend/tsconfig.spec.json +8 -0
- templates/angular/scaffold-boundary.yaml +27 -0
- templates/angular/skills/angular/SKILL.md +79 -0
- templates/angular/skills/angular/references/anatomy.md +69 -0
- templates/angular/stack.yaml +75 -0
- templates/aspnet-core/rules/backend.md +20 -0
- templates/aspnet-core/scaffold/docs/engineering/aspnet-core-rules.md +36 -0
- templates/aspnet-core/scaffold/docs/playbooks/aspnet-core-service.md +40 -0
- templates/aspnet-core/scaffold/src/backend/Backend.csproj +11 -0
- templates/aspnet-core/scaffold/src/backend/Backend.sln +27 -0
- templates/aspnet-core/scaffold/src/backend/Common/ExceptionHandlingMiddleware.cs +35 -0
- templates/aspnet-core/scaffold/src/backend/Features/Health/HealthEndpoints.cs +10 -0
- templates/aspnet-core/scaffold/src/backend/Features/Health/HealthService.cs +9 -0
- templates/aspnet-core/scaffold/src/backend/Program.cs +22 -0
- templates/aspnet-core/scaffold/src/backend/tests/Backend.Tests/Backend.Tests.csproj +22 -0
- templates/aspnet-core/scaffold/src/backend/tests/Backend.Tests/HealthServiceTests.cs +15 -0
- templates/aspnet-core/scaffold-boundary.yaml +24 -0
- templates/aspnet-core/skills/aspnet-core/SKILL.md +80 -0
- templates/aspnet-core/skills/aspnet-core/references/anatomy.md +65 -0
- templates/aspnet-core/stack.yaml +68 -0
- templates/astro/rules/frontend.md +20 -0
- templates/astro/scaffold/docs/engineering/astro-rules.md +40 -0
- templates/astro/scaffold/docs/playbooks/astro-app.md +57 -0
- templates/astro/scaffold/docs/playbooks/content-seo.md +37 -0
- templates/astro/scaffold/src/frontend/astro.config.mjs +10 -0
- templates/astro/scaffold/src/frontend/package.json +23 -0
- templates/astro/scaffold/src/frontend/src/components/Greeting.astro +13 -0
- templates/astro/scaffold/src/frontend/src/content/posts/hello.md +10 -0
- templates/astro/scaffold/src/frontend/src/content.config.ts +19 -0
- templates/astro/scaffold/src/frontend/src/lib/problem.test.ts +39 -0
- templates/astro/scaffold/src/frontend/src/lib/problem.ts +30 -0
- templates/astro/scaffold/src/frontend/src/pages/api/health.ts +13 -0
- templates/astro/scaffold/src/frontend/src/pages/index.astro +21 -0
- templates/astro/scaffold/src/frontend/tsconfig.json +9 -0
- templates/astro/scaffold/src/frontend/vitest.config.ts +10 -0
- templates/astro/scaffold-boundary.yaml +28 -0
- templates/astro/skills/astro/SKILL.md +71 -0
- templates/astro/skills/astro/references/anatomy.md +66 -0
- templates/astro/stack.yaml +75 -0
- templates/csharp-plain/scaffold/src/backend/Backend.csproj +12 -0
- templates/csharp-plain/scaffold/src/backend/Program.cs +1 -0
- templates/csharp-plain/scaffold-boundary.yaml +23 -0
- templates/csharp-plain/stack.yaml +50 -0
- templates/django/rules/backend.md +18 -0
- templates/django/scaffold/docs/engineering/anti-ambiguity.md +74 -0
- templates/django/scaffold/docs/engineering/backend-rules.md +133 -0
- templates/django/scaffold/docs/engineering/glossary.md +51 -0
- templates/django/scaffold/docs/engineering/logging-standards.md +107 -0
- templates/django/scaffold/docs/engineering/naming-conventions.md +68 -0
- templates/django/scaffold/docs/engineering/secrets-rotation-runbook.md +142 -0
- templates/django/scaffold/docs/playbooks/backend-api.md +119 -0
- templates/django/scaffold/src/backend/config/__init__.py +0 -0
- templates/django/scaffold/src/backend/config/settings.py +31 -0
- templates/django/scaffold/src/backend/config/urls.py +11 -0
- templates/django/scaffold/src/backend/config/wsgi.py +6 -0
- templates/django/scaffold/src/backend/manage.py +14 -0
- templates/django/scaffold/src/backend/pyproject.toml +37 -0
- templates/django/scaffold/src/backend/tests/test_health.py +4 -0
- templates/django/scaffold-boundary.yaml +25 -0
- templates/django/skills/python-django/SKILL.md +450 -0
- templates/django/skills/python-django/references/anatomy.md +117 -0
- templates/django/skills/python-django/scripts/new_endpoint.py +89 -0
- templates/django/stack.yaml +73 -0
- templates/fastapi/rules/backend.md +18 -0
- templates/fastapi/scaffold/docs/engineering/fastapi-rules.md +37 -0
- templates/fastapi/scaffold/docs/playbooks/fastapi-service.md +30 -0
- templates/fastapi/scaffold/src/backend/app/__init__.py +0 -0
- templates/fastapi/scaffold/src/backend/app/main.py +8 -0
- templates/fastapi/scaffold/src/backend/pyproject.toml +39 -0
- templates/fastapi/scaffold/src/backend/tests/test_health.py +11 -0
- templates/fastapi/scaffold-boundary.yaml +25 -0
- templates/fastapi/skills/python-fastapi/SKILL.md +75 -0
- templates/fastapi/skills/python-fastapi/references/anatomy.md +117 -0
- templates/fastapi/skills/python-fastapi/scripts/new_endpoint.py +101 -0
- templates/fastapi/stack.yaml +57 -0
- templates/flutter/rules/mobile.md +26 -0
- templates/flutter/scaffold/docs/engineering/flutter-rules.md +35 -0
- templates/flutter/scaffold/docs/playbooks/flutter-app.md +45 -0
- templates/flutter/scaffold/src/mobile/lib/core/error_mapper.dart +17 -0
- templates/flutter/scaffold/src/mobile/lib/core/router.dart +13 -0
- templates/flutter/scaffold/src/mobile/lib/main.dart +22 -0
- templates/flutter/scaffold/src/mobile/lib/screens/health_screen.dart +32 -0
- templates/flutter/scaffold/src/mobile/lib/services/health_service.dart +11 -0
- templates/flutter/scaffold/src/mobile/lib/state/health_provider.dart +13 -0
- templates/flutter/scaffold/src/mobile/pubspec.yaml +22 -0
- templates/flutter/scaffold/src/mobile/test/health_provider_test.dart +90 -0
- templates/flutter/scaffold-boundary.yaml +28 -0
- templates/flutter/skills/flutter/SKILL.md +76 -0
- templates/flutter/skills/flutter/references/anatomy.md +64 -0
- templates/flutter/stack.yaml +70 -0
- templates/go/rules/backend.md +19 -0
- templates/go/scaffold/docs/engineering/go-rules.md +45 -0
- templates/go/scaffold/docs/playbooks/go-service.md +30 -0
- templates/go/scaffold/src/backend/cmd/api/main.go +22 -0
- templates/go/scaffold/src/backend/cmd/api/main_test.go +20 -0
- templates/go/scaffold/src/backend/go.mod +3 -0
- templates/go/scaffold-boundary.yaml +25 -0
- templates/go/skills/go-patterns/SKILL.md +68 -0
- templates/go/skills/go-patterns/assets/go-checklist.md +29 -0
- templates/go/skills/go-patterns/references/anatomy.md +115 -0
- templates/go/skills/go-patterns/references/go-2026-idioms.md +92 -0
- templates/go/skills/go-patterns/scripts/new_endpoint.py +117 -0
- templates/go/skills/go-patterns/versions.json +16 -0
- templates/go/stack.yaml +54 -0
- templates/go-fiber/rules/backend.md +20 -0
- templates/go-fiber/scaffold/docs/engineering/fiber-rules.md +97 -0
- templates/go-fiber/scaffold/docs/playbooks/fiber-service.md +149 -0
- templates/go-fiber/scaffold/src/backend/cmd/api/main.go +21 -0
- templates/go-fiber/scaffold/src/backend/cmd/api/main_test.go +17 -0
- templates/go-fiber/scaffold/src/backend/go.mod +23 -0
- templates/go-fiber/scaffold/src/backend/go.sum +49 -0
- templates/go-fiber/scaffold-boundary.yaml +26 -0
- templates/go-fiber/skills/go-fiber/SKILL.md +203 -0
- templates/go-fiber/skills/go-fiber/assets/fiber-checklist.md +26 -0
- templates/go-fiber/skills/go-fiber/references/anatomy.md +116 -0
- templates/go-fiber/skills/go-fiber/references/fiber-v3-patterns.md +89 -0
- templates/go-fiber/skills/go-fiber/scripts/new_endpoint.py +107 -0
- templates/go-fiber/skills/go-fiber/versions.json +16 -0
- templates/go-fiber/stack.yaml +59 -0
- templates/go-plain/scaffold/src/backend/go.mod +3 -0
- templates/go-plain/scaffold/src/backend/main.go +7 -0
- templates/go-plain/scaffold-boundary.yaml +22 -0
- templates/go-plain/stack.yaml +51 -0
- templates/java-plain/scaffold/src/backend/mvnw +302 -0
- templates/java-plain/scaffold/src/backend/pom.xml +41 -0
- templates/java-plain/scaffold/src/backend/src/main/java/com/example/app/Main.java +10 -0
- templates/java-plain/scaffold-boundary.yaml +23 -0
- templates/java-plain/stack.yaml +51 -0
- templates/laravel/rules/backend.md +20 -0
- templates/laravel/scaffold/docs/engineering/laravel-rules.md +28 -0
- templates/laravel/scaffold/docs/playbooks/laravel-service.md +28 -0
- templates/laravel/scaffold/src/backend/app/Exceptions/Handler.php +27 -0
- templates/laravel/scaffold/src/backend/app/Http/Controllers/HealthController.php +15 -0
- templates/laravel/scaffold/src/backend/app/Support/HealthStatus.php +14 -0
- templates/laravel/scaffold/src/backend/composer.json +24 -0
- templates/laravel/scaffold/src/backend/phpunit.xml +10 -0
- templates/laravel/scaffold/src/backend/public/index.php +8 -0
- templates/laravel/scaffold/src/backend/routes/api.php +7 -0
- templates/laravel/scaffold/src/backend/tests/Unit/HealthStatusTest.php +16 -0
- templates/laravel/scaffold-boundary.yaml +23 -0
- templates/laravel/skills/laravel/SKILL.md +56 -0
- templates/laravel/skills/laravel/references/anatomy.md +65 -0
- templates/laravel/stack.yaml +66 -0
- templates/meta/rules/graph-first.md +27 -0
- templates/meta/rules/hook-author.md +19 -0
- templates/meta/rules/mcp-tool-author.md +18 -0
- templates/meta/rules/meta-engineering.md +17 -0
- templates/meta/scaffold-boundary.yaml +55 -0
- templates/meta/skills/claude-sdk-integration/SKILL.md +163 -0
- templates/meta/skills/claude-sdk-integration/assets/sdk-checklist.md +27 -0
- templates/meta/skills/claude-sdk-integration/scripts/check_model_ids.py +97 -0
- templates/meta/skills/graph-os-authoring/SKILL.md +278 -0
- templates/meta/skills/graph-os-authoring/assets/graph-os-checklist.md +25 -0
- templates/meta/skills/graph-os-authoring/scripts/new_extractor.py +76 -0
- templates/meta/skills/hook-authoring/SKILL.md +292 -0
- templates/meta/skills/hook-authoring/assets/hook-checklist.md +30 -0
- templates/meta/skills/hook-authoring/scripts/new_hook.sh +75 -0
- templates/meta/skills/mcp-tool-authoring/SKILL.md +301 -0
- templates/meta/skills/mcp-tool-authoring/assets/mcp-tool-checklist.md +29 -0
- templates/meta/skills/mcp-tool-authoring/scripts/new_tool.py +74 -0
- templates/meta/skills/meta-engineering/SKILL.md +151 -0
- templates/meta/skills/meta-engineering/assets/meta-edit-checklist.md +28 -0
- templates/meta/skills/meta-engineering/scripts/which_layer.py +61 -0
- templates/meta/skills/python-meta-server/SKILL.md +162 -0
- templates/meta/skills/python-meta-server/assets/meta-server-checklist.md +28 -0
- templates/meta/skills/python-meta-server/scripts/check_envelope.py +91 -0
- templates/meta/skills/react-vite-hub/SKILL.md +140 -0
- templates/meta/skills/react-vite-hub/assets/hub-ui-checklist.md +23 -0
- templates/meta/skills/react-vite-hub/scripts/check_vite_env.py +73 -0
- templates/meta/stack.yaml +119 -0
- templates/nestjs/rules/backend.md +20 -0
- templates/nestjs/scaffold/docs/engineering/nestjs-rules.md +33 -0
- templates/nestjs/scaffold/docs/playbooks/nestjs-service.md +39 -0
- templates/nestjs/scaffold/src/backend/nest-cli.json +5 -0
- templates/nestjs/scaffold/src/backend/package.json +28 -0
- templates/nestjs/scaffold/src/backend/src/app.module.ts +9 -0
- templates/nestjs/scaffold/src/backend/src/common/all-exceptions.filter.ts +59 -0
- templates/nestjs/scaffold/src/backend/src/health/health.controller.ts +14 -0
- templates/nestjs/scaffold/src/backend/src/health/health.module.ts +10 -0
- templates/nestjs/scaffold/src/backend/src/health/health.service.spec.ts +21 -0
- templates/nestjs/scaffold/src/backend/src/health/health.service.ts +9 -0
- templates/nestjs/scaffold/src/backend/src/main.ts +26 -0
- templates/nestjs/scaffold/src/backend/tsconfig.json +16 -0
- templates/nestjs/scaffold/src/backend/vitest.config.ts +9 -0
- templates/nestjs/scaffold-boundary.yaml +25 -0
- templates/nestjs/skills/nestjs/SKILL.md +67 -0
- templates/nestjs/skills/nestjs/references/anatomy.md +65 -0
- templates/nestjs/stack.yaml +68 -0
- templates/nextjs/rules/frontend.md +18 -0
- templates/nextjs/scaffold/docs/design/00-index.md +23 -0
- templates/nextjs/scaffold/docs/design/colors-tokens.md +141 -0
- templates/nextjs/scaffold/docs/design/components-patterns.md +159 -0
- templates/nextjs/scaffold/docs/design/motion-accessibility.md +137 -0
- templates/nextjs/scaffold/docs/design/typography-spacing.md +107 -0
- templates/nextjs/scaffold/docs/engineering/accessibility-web.md +56 -0
- templates/nextjs/scaffold/docs/engineering/copywriting-standard.md +102 -0
- templates/nextjs/scaffold/docs/engineering/formatting-rules.md +89 -0
- templates/nextjs/scaffold/docs/engineering/frontend-rendering-rules.md +80 -0
- templates/nextjs/scaffold/docs/engineering/frontend-rules.md +183 -0
- templates/nextjs/scaffold/docs/engineering/i18n-policy.md +99 -0
- templates/nextjs/scaffold/docs/pages-content-spec/00-index.md +78 -0
- templates/nextjs/scaffold/docs/playbooks/content-seo.md +55 -0
- templates/nextjs/scaffold/docs/playbooks/docs-governance.md +51 -0
- templates/nextjs/scaffold/docs/playbooks/frontend-ui.md +63 -0
- templates/nextjs/scaffold/src/frontend/app/layout.tsx +14 -0
- templates/nextjs/scaffold/src/frontend/app/page.tsx +3 -0
- templates/nextjs/scaffold/src/frontend/eslint.config.js +17 -0
- templates/nextjs/scaffold/src/frontend/lib/greeting.test.ts +9 -0
- templates/nextjs/scaffold/src/frontend/lib/greeting.ts +3 -0
- templates/nextjs/scaffold/src/frontend/package.json +28 -0
- templates/nextjs/scaffold/src/frontend/tsconfig.json +18 -0
- templates/nextjs/scaffold/src/frontend/vitest.config.ts +10 -0
- templates/nextjs/scaffold-boundary.yaml +30 -0
- templates/nextjs/skills/nextjs-react/SKILL.md +485 -0
- templates/nextjs/skills/nextjs-react/references/anatomy.md +116 -0
- templates/nextjs/skills/nextjs-react/scripts/new_component.py +72 -0
- templates/nextjs/stack.yaml +81 -0
- templates/node-express/rules/backend.md +20 -0
- templates/node-express/scaffold/docs/engineering/express-rules.md +30 -0
- templates/node-express/scaffold/docs/playbooks/express-service.md +35 -0
- templates/node-express/scaffold/src/backend/package.json +24 -0
- templates/node-express/scaffold/src/backend/src/index.ts +17 -0
- templates/node-express/scaffold/src/backend/src/middleware/error-handler.ts +12 -0
- templates/node-express/scaffold/src/backend/src/routes/health.test.ts +33 -0
- templates/node-express/scaffold/src/backend/src/routes/health.ts +7 -0
- templates/node-express/scaffold/src/backend/tsconfig.json +15 -0
- templates/node-express/scaffold/src/backend/types/express-bootstrap.d.ts +21 -0
- templates/node-express/scaffold-boundary.yaml +25 -0
- templates/node-express/skills/node-express/SKILL.md +70 -0
- templates/node-express/skills/node-express/references/anatomy.md +63 -0
- templates/node-express/stack.yaml +63 -0
- templates/python/scaffold/docs/engineering/python-rules.md +27 -0
- templates/python/scaffold/docs/playbooks/python-library.md +35 -0
- templates/python/stack.yaml +60 -0
- templates/rails/rules/backend.md +10 -0
- templates/rails/scaffold/docs/engineering/rails-rules.md +33 -0
- templates/rails/scaffold/docs/playbooks/rails-service.md +42 -0
- templates/rails/scaffold/src/backend/Gemfile +12 -0
- templates/rails/scaffold/src/backend/app/controllers/application_controller.rb +26 -0
- templates/rails/scaffold/src/backend/app/controllers/health_controller.rb +6 -0
- templates/rails/scaffold/src/backend/app/models/health.rb +6 -0
- templates/rails/scaffold/src/backend/config/application.rb +12 -0
- templates/rails/scaffold/src/backend/config/boot.rb +3 -0
- templates/rails/scaffold/src/backend/config/routes.rb +4 -0
- templates/rails/scaffold/src/backend/config.ru +5 -0
- templates/rails/scaffold/src/backend/spec/rails_helper.rb +18 -0
- templates/rails/scaffold/src/backend/spec/requests/health_spec.rb +24 -0
- templates/rails/scaffold-boundary.yaml +25 -0
- templates/rails/skills/rails/SKILL.md +62 -0
- templates/rails/skills/rails/references/anatomy.md +71 -0
- templates/rails/stack.yaml +72 -0
- templates/react-native/rules/mobile.md +26 -0
- templates/react-native/scaffold/docs/engineering/accessibility-mobile.md +95 -0
- templates/react-native/scaffold/docs/engineering/mobile-rules.md +56 -0
- templates/react-native/scaffold/docs/engineering/offline-first.md +61 -0
- templates/react-native/scaffold/docs/playbooks/mobile-app.md +49 -0
- templates/react-native/scaffold/src/mobile/App.tsx +9 -0
- templates/react-native/scaffold/src/mobile/eslint.config.js +17 -0
- templates/react-native/scaffold/src/mobile/package.json +23 -0
- templates/react-native/scaffold/src/mobile/src/greeting.test.ts +9 -0
- templates/react-native/scaffold/src/mobile/src/greeting.ts +3 -0
- templates/react-native/scaffold/src/mobile/tsconfig.json +17 -0
- templates/react-native/scaffold/src/mobile/vitest.config.ts +10 -0
- templates/react-native/scaffold-boundary.yaml +30 -0
- templates/react-native/skills/react-native-mobile/SKILL.md +119 -0
- templates/react-native/skills/react-native-mobile/assets/rn-mobile-checklist.md +28 -0
- templates/react-native/skills/react-native-mobile/references/anatomy.md +140 -0
- templates/react-native/skills/react-native-mobile/references/rn-2026-practices.md +54 -0
- templates/react-native/skills/react-native-mobile/scripts/new_screen.py +73 -0
- templates/react-native/skills/react-native-mobile/versions.json +16 -0
- templates/react-native/skills/react-native-patterns/SKILL.md +512 -0
- templates/react-native/skills/react-native-patterns/assets/rn-review-checklist.md +26 -0
- templates/react-native/skills/react-native-patterns/references/anatomy.md +62 -0
- templates/react-native/skills/react-native-patterns/references/list-performance.md +70 -0
- templates/react-native/skills/react-native-patterns/scripts/scan_rn_perf.py +77 -0
- templates/react-native/stack.yaml +70 -0
- templates/ruby-plain/scaffold/src/backend/Gemfile +8 -0
- templates/ruby-plain/scaffold/src/backend/main.rb +3 -0
- templates/ruby-plain/scaffold-boundary.yaml +23 -0
- templates/ruby-plain/stack.yaml +50 -0
- templates/rust-axum/rules/backend.md +20 -0
- templates/rust-axum/scaffold/docs/engineering/rust-axum-rules.md +35 -0
- templates/rust-axum/scaffold/docs/playbooks/rust-axum-service.md +43 -0
- templates/rust-axum/scaffold/src/backend/Cargo.toml +20 -0
- templates/rust-axum/scaffold/src/backend/src/app.rs +12 -0
- templates/rust-axum/scaffold/src/backend/src/error.rs +48 -0
- templates/rust-axum/scaffold/src/backend/src/main.rs +24 -0
- templates/rust-axum/scaffold/src/backend/src/routes/health.rs +37 -0
- templates/rust-axum/scaffold/src/backend/src/routes/mod.rs +2 -0
- templates/rust-axum/scaffold-boundary.yaml +25 -0
- templates/rust-axum/skills/rust/SKILL.md +73 -0
- templates/rust-axum/skills/rust/references/anatomy.md +63 -0
- templates/rust-axum/stack.yaml +65 -0
- templates/rust-plain/scaffold/src/backend/Cargo.toml +6 -0
- templates/rust-plain/scaffold/src/backend/src/main.rs +3 -0
- templates/rust-plain/scaffold-boundary.yaml +23 -0
- templates/rust-plain/stack.yaml +51 -0
- templates/spring-boot/rules/backend.md +20 -0
- templates/spring-boot/scaffold/docs/engineering/spring-boot-rules.md +35 -0
- templates/spring-boot/scaffold/docs/playbooks/spring-boot-service.md +45 -0
- templates/spring-boot/scaffold/src/backend/mvnw +302 -0
- templates/spring-boot/scaffold/src/backend/pom.xml +69 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/Application.java +16 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/common/GlobalExceptionHandler.java +31 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/health/HealthController.java +22 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/health/HealthService.java +12 -0
- templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/health/HealthStatus.java +4 -0
- templates/spring-boot/scaffold/src/backend/src/test/java/com/example/app/health/HealthServiceTest.java +15 -0
- templates/spring-boot/scaffold-boundary.yaml +26 -0
- templates/spring-boot/skills/spring-boot/SKILL.md +84 -0
- templates/spring-boot/skills/spring-boot/references/anatomy.md +63 -0
- templates/spring-boot/stack.yaml +65 -0
- templates/svelte-sveltekit/rules/frontend.md +20 -0
- templates/svelte-sveltekit/scaffold/docs/engineering/svelte-sveltekit-rules.md +37 -0
- templates/svelte-sveltekit/scaffold/docs/playbooks/svelte-sveltekit-app.md +36 -0
- templates/svelte-sveltekit/scaffold/src/frontend/package.json +22 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/app.html +12 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/hooks.server.ts +14 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/lib/components/Greeting.svelte +6 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/lib/stores/count.test.ts +26 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/lib/stores/count.ts +4 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/routes/+layout.svelte +24 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/routes/+page.svelte +9 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/routes/+page.ts +7 -0
- templates/svelte-sveltekit/scaffold/src/frontend/src/routes/health/+server.ts +7 -0
- templates/svelte-sveltekit/scaffold/src/frontend/svelte.config.js +10 -0
- templates/svelte-sveltekit/scaffold/src/frontend/tsconfig.json +7 -0
- templates/svelte-sveltekit/scaffold/src/frontend/vite.config.ts +7 -0
- templates/svelte-sveltekit/scaffold/src/frontend/vitest.config.ts +11 -0
- templates/svelte-sveltekit/scaffold-boundary.yaml +29 -0
- templates/svelte-sveltekit/skills/svelte/SKILL.md +90 -0
- templates/svelte-sveltekit/skills/svelte/references/anatomy.md +62 -0
- templates/svelte-sveltekit/stack.yaml +70 -0
- templates/typescript-plain/scaffold/src/index.ts +3 -0
- templates/typescript-plain/scaffold/tsconfig.json +13 -0
- templates/typescript-plain/scaffold-boundary.yaml +22 -0
- templates/typescript-plain/stack.yaml +44 -0
- templates/vue-nuxt/rules/frontend.md +19 -0
- templates/vue-nuxt/scaffold/docs/engineering/nuxt-rules.md +30 -0
- templates/vue-nuxt/scaffold/docs/playbooks/nuxt-app.md +29 -0
- templates/vue-nuxt/scaffold/src/frontend/app.vue +3 -0
- templates/vue-nuxt/scaffold/src/frontend/nuxt.config.ts +11 -0
- templates/vue-nuxt/scaffold/src/frontend/package.json +20 -0
- templates/vue-nuxt/scaffold/src/frontend/pages/index.test.ts +19 -0
- templates/vue-nuxt/scaffold/src/frontend/pages/index.vue +11 -0
- templates/vue-nuxt/scaffold/src/frontend/vitest.config.ts +12 -0
- templates/vue-nuxt/scaffold-boundary.yaml +26 -0
- templates/vue-nuxt/skills/vue-nuxt/SKILL.md +57 -0
- templates/vue-nuxt/skills/vue-nuxt/references/anatomy.md +60 -0
- templates/vue-nuxt/stack.yaml +61 -0
- templates/wordpress/rules/backend.md +19 -0
- templates/wordpress/scaffold/docs/engineering/wordpress-rules.md +28 -0
- templates/wordpress/scaffold/docs/playbooks/wordpress-service.md +29 -0
- templates/wordpress/scaffold/src/backend/composer.json +17 -0
- templates/wordpress/scaffold/src/backend/phpcs.xml.dist +11 -0
- templates/wordpress/scaffold/src/backend/phpunit.xml +10 -0
- templates/wordpress/scaffold/src/backend/plugin/inc/health.php +8 -0
- templates/wordpress/scaffold/src/backend/plugin/plugin.php +28 -0
- templates/wordpress/scaffold/src/backend/tests/HealthStatusTest.php +15 -0
- templates/wordpress/scaffold/src/backend/theme/functions.php +18 -0
- templates/wordpress/scaffold/src/backend/theme/style.css +11 -0
- templates/wordpress/scaffold-boundary.yaml +23 -0
- templates/wordpress/skills/wordpress/SKILL.md +110 -0
- templates/wordpress/skills/wordpress/assets/wp-checklist.md +28 -0
- templates/wordpress/skills/wordpress/references/wp-development.md +75 -0
- templates/wordpress/skills/wordpress/references/wp-security.md +68 -0
- templates/wordpress/skills/wordpress/scripts/scan_wp_smells.py +91 -0
- templates/wordpress/skills/wordpress/versions.json +16 -0
- templates/wordpress/stack.yaml +60 -0
- thinking_os/__init__.py +1 -0
- thinking_os/_agent_markers.py +32 -0
- thinking_os/background.py +405 -0
- thinking_os/bootstrap_outcomes.py +200 -0
- thinking_os/budget.py +302 -0
- thinking_os/capture.py +495 -0
- thinking_os/cognition.py +516 -0
- thinking_os/cognition_schemas.py +517 -0
- thinking_os/compress.py +192 -0
- thinking_os/concepts.py +233 -0
- thinking_os/dashboard.py +159 -0
- thinking_os/database.py +2883 -0
- thinking_os/decay.py +393 -0
- thinking_os/digest.py +295 -0
- thinking_os/dispatcher.py +192 -0
- thinking_os/dispatcher_helpers.py +48 -0
- thinking_os/dispatchers/__init__.py +3 -0
- thinking_os/dispatchers/default.py +47 -0
- thinking_os/distill.py +192 -0
- thinking_os/doc_indexer.py +905 -0
- thinking_os/embeddings.py +943 -0
- thinking_os/formula_composer.py +556 -0
- thinking_os/gate_marker.py +75 -0
- thinking_os/graph.py +296 -0
- thinking_os/graph_indexer.py +360 -0
- thinking_os/health_check.py +517 -0
- thinking_os/impact.py +119 -0
- thinking_os/memory_gc.py +356 -0
- thinking_os/migrator_embeddings.py +318 -0
- thinking_os/precision.py +194 -0
- thinking_os/record_outcome.py +399 -0
- thinking_os/repair.py +105 -0
- thinking_os/retrieval_quality.py +239 -0
- thinking_os/roles_state.py +168 -0
- thinking_os/sanitizer.py +320 -0
- thinking_os/server.py +3160 -0
- thinking_os/session_enrich.py +272 -0
- thinking_os/session_observe_worker.py +111 -0
- thinking_os/session_startup.py +74 -0
- thinking_os/session_summary.py +245 -0
- thinking_os/task_analyzer.py +462 -0
- thinking_os/task_parser.py +342 -0
- thinking_os/task_sync.py +73 -0
- thinking_os/tools/__init__.py +6 -0
- thinking_os/tools/_shared.py +947 -0
- thinking_os/tools/cognition.py +1867 -0
- thinking_os/tools/docs.py +770 -0
- thinking_os/tools/learning.py +2078 -0
- thinking_os/tools/logs.py +79 -0
- thinking_os/tools/memory.py +840 -0
- thinking_os/tools/metrics.py +200 -0
- thinking_os/tools/retrieve.py +415 -0
- thinking_os/tools/routing.py +658 -0
- thinking_os/tools/tasks.py +449 -0
- thinking_os/tools/trajectory.py +181 -0
- thinking_os/tracing.py +235 -0
- web/__init__.py +5 -0
- web/_cache.py +118 -0
- web/_deps.py +56 -0
- web/_envelope.py +85 -0
- web/_project_context.py +140 -0
- web/chat_providers.py +108 -0
- web/init_jobs.py +216 -0
- web/routes/__init__.py +25 -0
- web/routes/_bounded_read.py +76 -0
- web/routes/board.py +1089 -0
- web/routes/cognition.py +1838 -0
- web/routes/config.py +635 -0
- web/routes/graph.py +513 -0
- web/routes/health.py +180 -0
- web/routes/hooks.py +288 -0
- web/routes/hub.py +1219 -0
- web/routes/logs.py +374 -0
- web/routes/metrics.py +43 -0
- web/routes/observability.py +400 -0
- web/routes/patterns.py +227 -0
- web/routes/presence.py +609 -0
- web/routes/roles.py +446 -0
- web/routes/scheduled.py +261 -0
- web/routes/search.py +238 -0
- web/routes/sessions.py +220 -0
- web/routes/settings.py +363 -0
- web/routes/stream.py +547 -0
- web/security.py +157 -0
- web/server.py +283 -0
thinking_os/server.py
ADDED
|
@@ -0,0 +1,3160 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Coding OS — Thinking OS MCP Server (stdio transport).
|
|
4
|
+
|
|
5
|
+
Agent-agnostic self-learning system for AI coding agents.
|
|
6
|
+
Tools live in the modules under tools/; board_os and graph_os tools are
|
|
7
|
+
mounted onto the same FastMCP server when their packages are importable.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from database import get_db_stats, get_pooled_conn, init_db, project_root
|
|
19
|
+
from mcp.server.fastmcp import FastMCP
|
|
20
|
+
from tools._shared import apply_module_tool_gating, fail, ok, safe_tool
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Logging — central via core.logging_os; .mcp.log retained as MCP-specific sink.
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
from core.logging_os import setup as _logging_os_setup
|
|
26
|
+
|
|
27
|
+
_logging_os_setup(level="info")
|
|
28
|
+
logger = logging.getLogger("thinking_os")
|
|
29
|
+
|
|
30
|
+
_LOG_FORMAT = "%(asctime)s [%(name)s] %(levelname)s: %(message)s"
|
|
31
|
+
try:
|
|
32
|
+
_state_dir = Path(os.environ.get("COS_STATE_DIR") or ".coding-os")
|
|
33
|
+
_state_dir.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
_file_handler = logging.FileHandler(_state_dir / ".mcp.log", mode="a", encoding="utf-8")
|
|
35
|
+
_file_handler.setFormatter(logging.Formatter(_LOG_FORMAT))
|
|
36
|
+
logging.getLogger().addHandler(_file_handler)
|
|
37
|
+
except OSError as _exc:
|
|
38
|
+
logger.debug("mcp log file mirror unavailable: %s", _exc)
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# MCP Server
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
mcp = FastMCP("coding_os_mcp")
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Database bootstrap
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
_db_conn = init_db()
|
|
49
|
+
|
|
50
|
+
# Opt-in continuous indexer. No-op unless COS_BACKGROUND_INDEX=1.
|
|
51
|
+
# Wrapped in try/except so a broken indexer never blocks MCP startup.
|
|
52
|
+
try:
|
|
53
|
+
from background import maybe_start_indexer
|
|
54
|
+
|
|
55
|
+
_bg_status = maybe_start_indexer()
|
|
56
|
+
if _bg_status.get("started"):
|
|
57
|
+
logger.info("background indexer started: %s", _bg_status.get("reason"))
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
logger.warning("background indexer bootstrap failed: %s", exc)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Health check tool
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
@mcp.tool(
|
|
66
|
+
name="cos_health",
|
|
67
|
+
annotations={
|
|
68
|
+
"title": "Thinking OS Health Check",
|
|
69
|
+
"readOnlyHint": True,
|
|
70
|
+
"destructiveHint": False,
|
|
71
|
+
"idempotentHint": True,
|
|
72
|
+
"openWorldHint": False,
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
@safe_tool
|
|
76
|
+
def thinking_os_health() -> str:
|
|
77
|
+
"""Return database health stats: row counts per table, schema version, DB size, FTS5 availability, embeddings status.
|
|
78
|
+
|
|
79
|
+
Use this tool to verify the thinking_os database is operational and
|
|
80
|
+
to get a quick summary of stored data volume.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
str: JSON object with keys: tables (row counts), schema_version,
|
|
84
|
+
fts5_available, db_size_bytes, rag (embeddings + doc_chunks status).
|
|
85
|
+
"""
|
|
86
|
+
stats = get_db_stats(_db_conn)
|
|
87
|
+
|
|
88
|
+
# Surface RAG availability so the agent can decide whether
|
|
89
|
+
# semantic search is wired up before issuing cos_doc_search.
|
|
90
|
+
embeddings_available = False
|
|
91
|
+
active_model = "unknown"
|
|
92
|
+
try:
|
|
93
|
+
from embeddings import active_model_name, is_available
|
|
94
|
+
|
|
95
|
+
embeddings_available = is_available()
|
|
96
|
+
active_model = active_model_name()
|
|
97
|
+
except ImportError as exc:
|
|
98
|
+
logger.debug("Embeddings module unavailable for health check: %s", exc)
|
|
99
|
+
|
|
100
|
+
stats["rag"] = {
|
|
101
|
+
"embeddings_available": embeddings_available,
|
|
102
|
+
"embedding_model": active_model,
|
|
103
|
+
"embeddings_count": stats["tables"].get("embeddings") or 0,
|
|
104
|
+
"document_chunks_count": stats["tables"].get("document_chunks") or 0,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# Task store status — lets the agent detect whether
|
|
108
|
+
# `cos_task_*` queries will return data before making the call.
|
|
109
|
+
stats["task_store"] = {
|
|
110
|
+
"tasks_count": stats["tables"].get("tasks") or 0,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Background indexer status — surfaced even when the loop
|
|
114
|
+
# is disabled so `cos doctor` can warn about misconfigured state.
|
|
115
|
+
try:
|
|
116
|
+
from background import get_indexer, is_enabled
|
|
117
|
+
|
|
118
|
+
stats["background_indexer"] = (
|
|
119
|
+
get_indexer().status()
|
|
120
|
+
if is_enabled()
|
|
121
|
+
else {
|
|
122
|
+
"enabled": False,
|
|
123
|
+
"running": False,
|
|
124
|
+
"reason": "COS_BACKGROUND_INDEX not set",
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
except ImportError as exc: # pragma: no cover — defensive
|
|
128
|
+
logger.debug("background module unavailable: %s", exc)
|
|
129
|
+
stats["background_indexer"] = {
|
|
130
|
+
"enabled": False,
|
|
131
|
+
"running": False,
|
|
132
|
+
"reason": f"import_error: {exc}",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Constitution slice durability (TASK-497): the values layer is surfaced at
|
|
136
|
+
# every SessionStart directly from docs/governance/constitution.md — not from
|
|
137
|
+
# decaying agent memory — so it is non-decaying by construction. A missing file
|
|
138
|
+
# or absent SLICE markers would silently drop the slice; assert it here so
|
|
139
|
+
# `cos doctor` / health flags the regression like a dangling symlink. Fail-open.
|
|
140
|
+
try:
|
|
141
|
+
const_path = project_root() / "docs" / "governance" / "constitution.md"
|
|
142
|
+
present = const_path.is_file()
|
|
143
|
+
slice_markers_ok = False
|
|
144
|
+
if present:
|
|
145
|
+
const_text = const_path.read_text(encoding="utf-8", errors="ignore")
|
|
146
|
+
start = const_text.find("<!-- SLICE:START -->")
|
|
147
|
+
end = const_text.find("<!-- SLICE:END -->")
|
|
148
|
+
# Require non-empty CONTENT between the markers, not just their
|
|
149
|
+
# presence: an empty slice silently delivers no values while a
|
|
150
|
+
# markers-only check would report healthy (TASK-510).
|
|
151
|
+
slice_markers_ok = (
|
|
152
|
+
start != -1
|
|
153
|
+
and end > start
|
|
154
|
+
and const_text[start + len("<!-- SLICE:START -->") : end].strip() != ""
|
|
155
|
+
)
|
|
156
|
+
stats["constitution"] = {
|
|
157
|
+
"present": present,
|
|
158
|
+
"slice_markers_ok": slice_markers_ok,
|
|
159
|
+
"non_decaying": True,
|
|
160
|
+
"repair": None
|
|
161
|
+
if (present and slice_markers_ok)
|
|
162
|
+
else "restore docs/governance/constitution.md with a non-empty <!-- SLICE:START/END --> block so SessionStart can surface the values slice (TASK-491)",
|
|
163
|
+
}
|
|
164
|
+
except Exception as exc: # pragma: no cover — defensive, never fail the health call
|
|
165
|
+
logger.debug("constitution health check failed: %s", exc)
|
|
166
|
+
stats["constitution"] = {
|
|
167
|
+
"present": False,
|
|
168
|
+
"slice_markers_ok": False,
|
|
169
|
+
"non_decaying": True,
|
|
170
|
+
"repair": f"check_error: {exc}",
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return ok(stats, meta={"layer": "health"})
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
# Import tool modules
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
from graph import query_related
|
|
180
|
+
from tools.docs import doc_search, list_doc_headers, parse_doc_header
|
|
181
|
+
from tools.learning import (
|
|
182
|
+
learn_extract,
|
|
183
|
+
learn_narrative,
|
|
184
|
+
learn_suggest,
|
|
185
|
+
learn_validate,
|
|
186
|
+
)
|
|
187
|
+
from tools.logs import log_query
|
|
188
|
+
from tools.memory import memory_details, memory_promote, memory_search, memory_timeline
|
|
189
|
+
from tools.metrics import metric_query, metric_record, metric_trend
|
|
190
|
+
from tools.retrieve import (
|
|
191
|
+
cite_retrievals,
|
|
192
|
+
learn_from_retrievals,
|
|
193
|
+
log_retrieval,
|
|
194
|
+
log_router_decision,
|
|
195
|
+
)
|
|
196
|
+
from tools.routing import failure_pattern_query, route_model_bandit, route_skill
|
|
197
|
+
from tools.tasks import task_by_filter, task_dependencies, task_dependents, task_search
|
|
198
|
+
from tools.trajectory import trajectory_read, trajectory_snapshot
|
|
199
|
+
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
# Agent-session resolver — fix for AGENT STREAM "H" label
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _detect_agent_session_default() -> str | None:
|
|
206
|
+
"""Best-effort fallback for MCP tools that accept `agent_session`."""
|
|
207
|
+
import os as _os
|
|
208
|
+
from pathlib import Path as _P
|
|
209
|
+
|
|
210
|
+
explicit = (_os.environ.get("COS_AGENT_SESSION_ID") or "").strip()
|
|
211
|
+
if explicit:
|
|
212
|
+
return explicit
|
|
213
|
+
|
|
214
|
+
def _first_line(p: "_P") -> str:
|
|
215
|
+
try:
|
|
216
|
+
return p.read_text(encoding="utf-8", errors="ignore").strip() if p.is_file() else ""
|
|
217
|
+
except OSError:
|
|
218
|
+
return ""
|
|
219
|
+
|
|
220
|
+
# Priority 0 — the calling panel's own session-id, when a panel dir is
|
|
221
|
+
# in the environment (hook-driven CLI calls). Most accurate signal.
|
|
222
|
+
panel_dir_env = _os.environ.get("COS_PANEL_DIR")
|
|
223
|
+
if panel_dir_env:
|
|
224
|
+
sid = _first_line(_P(panel_dir_env) / "session-id")
|
|
225
|
+
if sid:
|
|
226
|
+
return sid
|
|
227
|
+
|
|
228
|
+
# Priority 1 — the agent-level ".active-session" pointer that
|
|
229
|
+
# session-context.sh refreshes every prompt. The long-lived MCP server
|
|
230
|
+
# has no $COS_PANEL_DIR, so this is the freshest signal it can read;
|
|
231
|
+
# the flat "session-id" file is a stale fossil kept only as a last
|
|
232
|
+
# resort (see docs/engineering/state-files.md).
|
|
233
|
+
agent_dir_env = _os.environ.get("COS_AGENT_DIR")
|
|
234
|
+
if agent_dir_env:
|
|
235
|
+
for _fname in (".active-session", "session-id"):
|
|
236
|
+
sid = _first_line(_P(agent_dir_env) / _fname)
|
|
237
|
+
if sid:
|
|
238
|
+
return sid
|
|
239
|
+
|
|
240
|
+
# Priority 2 — vendor env markers. Data-driven from
|
|
241
|
+
# adapters/<id>/adapter.yaml::runtime_env_markers (rule #11 — no
|
|
242
|
+
# hardcoded vendor lists in core).
|
|
243
|
+
agent: str | None = None
|
|
244
|
+
if _os.environ.get("COS_AGENT"):
|
|
245
|
+
agent = _os.environ["COS_AGENT"].strip().lower() or None
|
|
246
|
+
else:
|
|
247
|
+
try:
|
|
248
|
+
from board_os._agent_runtime import detect_agent as _detect_agent
|
|
249
|
+
|
|
250
|
+
detected = _detect_agent(None)
|
|
251
|
+
# detect_agent returns "agent" or "human" when nothing matches;
|
|
252
|
+
# only treat real adapter ids as a positive identification.
|
|
253
|
+
if detected and detected not in ("human", "agent"):
|
|
254
|
+
agent = detected
|
|
255
|
+
except Exception:
|
|
256
|
+
agent = None
|
|
257
|
+
# Fallback heuristic — CLAUDE_PROJECT_DIR is a weak signal, so it
|
|
258
|
+
# only fires when no stronger signal matched.
|
|
259
|
+
if agent is None and _os.environ.get("CLAUDE_PROJECT_DIR"):
|
|
260
|
+
agent = "claude"
|
|
261
|
+
|
|
262
|
+
if agent is None:
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
state_dir = _os.environ.get("COS_STATE_DIR", ".coding-os")
|
|
266
|
+
sid_path = _P(state_dir) / agent / "session-id"
|
|
267
|
+
try:
|
|
268
|
+
if sid_path.is_file():
|
|
269
|
+
raw = sid_path.read_text(encoding="utf-8", errors="ignore").strip()
|
|
270
|
+
if raw:
|
|
271
|
+
return raw
|
|
272
|
+
except OSError:
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
# Last resort — synthesize a per-process id so the column at least
|
|
276
|
+
# carries the agent prefix instead of NULL. The hub's
|
|
277
|
+
# `agentForSession()` substring-matches on "claude" / "codex",
|
|
278
|
+
# so this is enough to render the correct badge.
|
|
279
|
+
return f"ses-{agent}-mcp-{_os.getpid()}"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# Metrics tools
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
@mcp.tool(
|
|
286
|
+
name="cos_metric_record",
|
|
287
|
+
annotations={
|
|
288
|
+
"title": "Record Agent Metric",
|
|
289
|
+
"readOnlyHint": False,
|
|
290
|
+
"destructiveHint": False,
|
|
291
|
+
"idempotentHint": False,
|
|
292
|
+
"openWorldHint": False,
|
|
293
|
+
},
|
|
294
|
+
)
|
|
295
|
+
@safe_tool
|
|
296
|
+
def cos_metric_record(
|
|
297
|
+
agent_type: str,
|
|
298
|
+
outcome: str,
|
|
299
|
+
task_id: str = "",
|
|
300
|
+
model: str = "",
|
|
301
|
+
duration_ms: int = 0,
|
|
302
|
+
domain: str = "",
|
|
303
|
+
complexity: str = "",
|
|
304
|
+
) -> str:
|
|
305
|
+
"""Record a single agent performance metric after task completion.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
agent_type: Type of agent (e.g. "general", "planner", "code-reviewer").
|
|
309
|
+
outcome: Result — one of: success, rework, partial, blocked.
|
|
310
|
+
task_id: Task identifier (e.g. "TASK-143"). Optional.
|
|
311
|
+
model: Model used (e.g. "sonnet", "opus"). Optional.
|
|
312
|
+
duration_ms: Duration in milliseconds. Optional.
|
|
313
|
+
domain: Task domain (e.g. "BACKEND", "FRONTEND", "INFRA"). Optional.
|
|
314
|
+
complexity: Cynefin classification (e.g. "CLEAR", "COMPLICATED"). Optional.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
str: JSON with inserted row id and status.
|
|
318
|
+
"""
|
|
319
|
+
result = metric_record(
|
|
320
|
+
_db_conn,
|
|
321
|
+
task_id=task_id or None,
|
|
322
|
+
agent_type=agent_type,
|
|
323
|
+
model=model or None,
|
|
324
|
+
duration_ms=duration_ms or None,
|
|
325
|
+
outcome=outcome,
|
|
326
|
+
domain=domain or None,
|
|
327
|
+
complexity=complexity or None,
|
|
328
|
+
)
|
|
329
|
+
return ok(result, meta={"layer": "metrics"})
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@mcp.tool(
|
|
333
|
+
name="cos_metric_query",
|
|
334
|
+
annotations={
|
|
335
|
+
"title": "Query Agent Metrics",
|
|
336
|
+
"readOnlyHint": True,
|
|
337
|
+
"destructiveHint": False,
|
|
338
|
+
"idempotentHint": True,
|
|
339
|
+
"openWorldHint": False,
|
|
340
|
+
},
|
|
341
|
+
)
|
|
342
|
+
@safe_tool
|
|
343
|
+
def cos_metric_query(
|
|
344
|
+
domain: str = "",
|
|
345
|
+
model: str = "",
|
|
346
|
+
outcome: str = "",
|
|
347
|
+
agent_type: str = "",
|
|
348
|
+
date_from: str = "",
|
|
349
|
+
date_to: str = "",
|
|
350
|
+
limit: int = 20,
|
|
351
|
+
) -> str:
|
|
352
|
+
"""Query agent metrics with optional filters.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
domain: Filter by domain (e.g. "BACKEND"). Optional.
|
|
356
|
+
model: Filter by model (e.g. "sonnet"). Optional.
|
|
357
|
+
outcome: Filter by outcome (e.g. "rework"). Optional.
|
|
358
|
+
agent_type: Filter by agent type. Optional.
|
|
359
|
+
date_from: Start date (ISO format, e.g. "2026-03-01"). Optional.
|
|
360
|
+
date_to: End date (ISO format, e.g. "2026-03-25"). Optional.
|
|
361
|
+
limit: Max rows (1-100, default 20).
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
str: JSON with total count and matching rows.
|
|
365
|
+
"""
|
|
366
|
+
result = metric_query(
|
|
367
|
+
_db_conn,
|
|
368
|
+
domain=domain or None,
|
|
369
|
+
model=model or None,
|
|
370
|
+
outcome=outcome or None,
|
|
371
|
+
agent_type=agent_type or None,
|
|
372
|
+
date_from=date_from or None,
|
|
373
|
+
date_to=date_to or None,
|
|
374
|
+
limit=limit,
|
|
375
|
+
)
|
|
376
|
+
return ok(
|
|
377
|
+
result,
|
|
378
|
+
meta={
|
|
379
|
+
"layer": "metrics",
|
|
380
|
+
"filters_applied": {
|
|
381
|
+
"domain": domain or None,
|
|
382
|
+
"model": model or None,
|
|
383
|
+
"outcome": outcome or None,
|
|
384
|
+
"agent_type": agent_type or None,
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@mcp.tool(
|
|
391
|
+
name="cos_metric_trend",
|
|
392
|
+
annotations={
|
|
393
|
+
"title": "Agent Metric Trends",
|
|
394
|
+
"readOnlyHint": True,
|
|
395
|
+
"destructiveHint": False,
|
|
396
|
+
"idempotentHint": True,
|
|
397
|
+
"openWorldHint": False,
|
|
398
|
+
},
|
|
399
|
+
)
|
|
400
|
+
@safe_tool
|
|
401
|
+
def cos_metric_trend(
|
|
402
|
+
metric: str = "success_rate",
|
|
403
|
+
window_days: int = 30,
|
|
404
|
+
group_by: str = "domain",
|
|
405
|
+
) -> str:
|
|
406
|
+
"""Get aggregated trend data for agent metrics.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
metric: One of: success_rate, rework_rate, count.
|
|
410
|
+
window_days: Lookback window in days (1-365, default 30).
|
|
411
|
+
group_by: Grouping dimension: domain, model, agent_type, complexity.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
str: JSON with trends array containing period, counts, and rate.
|
|
415
|
+
"""
|
|
416
|
+
result = metric_trend(
|
|
417
|
+
_db_conn,
|
|
418
|
+
metric=metric,
|
|
419
|
+
window_days=window_days,
|
|
420
|
+
group_by=group_by,
|
|
421
|
+
)
|
|
422
|
+
return ok(result, meta={"layer": "metrics"})
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@mcp.tool(
|
|
426
|
+
name="cos_log_query",
|
|
427
|
+
annotations={
|
|
428
|
+
"title": "Query Durable Error / Log Store",
|
|
429
|
+
"readOnlyHint": True,
|
|
430
|
+
"destructiveHint": False,
|
|
431
|
+
"idempotentHint": True,
|
|
432
|
+
"openWorldHint": False,
|
|
433
|
+
},
|
|
434
|
+
)
|
|
435
|
+
@safe_tool
|
|
436
|
+
def cos_log_query(
|
|
437
|
+
level: str = "",
|
|
438
|
+
scope: str = "",
|
|
439
|
+
since: str = "",
|
|
440
|
+
search: str = "",
|
|
441
|
+
session_id: str = "",
|
|
442
|
+
trace_id: str = "",
|
|
443
|
+
fingerprint: str = "",
|
|
444
|
+
limit: int = 50,
|
|
445
|
+
) -> str:
|
|
446
|
+
"""Query the durable log_events store (WARN+), most-recent first — the agent's "what is broken now"."""
|
|
447
|
+
result = log_query(
|
|
448
|
+
_db_conn,
|
|
449
|
+
level=level or None,
|
|
450
|
+
scope=scope or None,
|
|
451
|
+
since=since or None,
|
|
452
|
+
search=search or None,
|
|
453
|
+
session_id=session_id or None,
|
|
454
|
+
trace_id=trace_id or None,
|
|
455
|
+
fingerprint=fingerprint or None,
|
|
456
|
+
limit=limit,
|
|
457
|
+
)
|
|
458
|
+
return ok(result, meta={"layer": "logs", "source": "cos_log_query"})
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
# ---------------------------------------------------------------------------
|
|
462
|
+
# Memory tools
|
|
463
|
+
# ---------------------------------------------------------------------------
|
|
464
|
+
@mcp.tool(
|
|
465
|
+
name="cos_observation_record",
|
|
466
|
+
annotations={
|
|
467
|
+
"title": "Record Observation (manual capture)",
|
|
468
|
+
"readOnlyHint": False,
|
|
469
|
+
"destructiveHint": False,
|
|
470
|
+
"idempotentHint": True,
|
|
471
|
+
"openWorldHint": False,
|
|
472
|
+
},
|
|
473
|
+
)
|
|
474
|
+
@safe_tool
|
|
475
|
+
def cos_observation_record(
|
|
476
|
+
file_path: str,
|
|
477
|
+
tool_name: str = "Edit",
|
|
478
|
+
) -> str:
|
|
479
|
+
"""Record an observation explicitly."""
|
|
480
|
+
from capture import capture_observation
|
|
481
|
+
|
|
482
|
+
tool_name = (tool_name or "Edit").strip()
|
|
483
|
+
if tool_name not in {"Write", "Edit", "MultiEdit"}:
|
|
484
|
+
return fail("validation", f"tool_name must be Write|Edit|MultiEdit, got {tool_name!r}")
|
|
485
|
+
if not file_path:
|
|
486
|
+
return fail("validation", "file_path is required")
|
|
487
|
+
payload = {"tool_name": tool_name, "tool_input": {"file_path": file_path}}
|
|
488
|
+
result = capture_observation(payload)
|
|
489
|
+
return ok(result, meta={"layer": "memory", "source": "cos_observation_record"})
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@mcp.tool(
|
|
493
|
+
name="cos_search",
|
|
494
|
+
annotations={
|
|
495
|
+
"title": "Search Thinking OS Memory",
|
|
496
|
+
"readOnlyHint": False, # writes retrieval telemetry only — raw search does NOT bump access_count/confidence
|
|
497
|
+
"destructiveHint": False,
|
|
498
|
+
"idempotentHint": False,
|
|
499
|
+
"openWorldHint": False,
|
|
500
|
+
},
|
|
501
|
+
)
|
|
502
|
+
@safe_tool(name="cos_search")
|
|
503
|
+
def thinking_os_search(
|
|
504
|
+
query: str,
|
|
505
|
+
limit: int = 5,
|
|
506
|
+
memory_type: str = "",
|
|
507
|
+
min_confidence: float = 0.3,
|
|
508
|
+
since_days: int = 0,
|
|
509
|
+
) -> str:
|
|
510
|
+
"""Search observations and learned patterns with 5-signal ranking.
|
|
511
|
+
|
|
512
|
+
Use during Orient step to find relevant past experience. Read-only over
|
|
513
|
+
memory rows (writes retrieval telemetry only; reinforcement happens on
|
|
514
|
+
cos_details, not here — TASK-109).
|
|
515
|
+
|
|
516
|
+
Stage-1 metadata pre-filter:
|
|
517
|
+
- `min_confidence` drops decayed/low-trust patterns BEFORE ranking.
|
|
518
|
+
Stale low-signal patterns can otherwise crowd out fresh hits.
|
|
519
|
+
Default 0.3 skips decayed/unvalidated noise (fresh patterns start at
|
|
520
|
+
0.5, so they still pass); pass 0.0 to include everything.
|
|
521
|
+
- `since_days` caps row age. 0 = no cap (default) — age is opt-in so a
|
|
522
|
+
valuable old decision is never silently hidden from default recall.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
query: Search text (e.g. "backend rework", "django migration").
|
|
526
|
+
limit: Max results (1-20, default 5).
|
|
527
|
+
memory_type: Filter by type (pattern/workflow/error/decision/discovery). Optional.
|
|
528
|
+
min_confidence: Drop learned_patterns with confidence below this
|
|
529
|
+
value (0.0-1.0). Default 0.3 (skips decayed noise). 0.0 = no filter.
|
|
530
|
+
since_days: Drop rows older than now-`since_days`. 0 = no cap.
|
|
531
|
+
Common: 90 (one quarter) for "recent" queries.
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
str: JSON with results list [{id, title, confidence, impact_score, memory_type, source_table}].
|
|
535
|
+
"""
|
|
536
|
+
from database import has_fts5_table
|
|
537
|
+
|
|
538
|
+
result = memory_search(
|
|
539
|
+
_db_conn,
|
|
540
|
+
query=query,
|
|
541
|
+
limit=limit,
|
|
542
|
+
memory_type=memory_type or None,
|
|
543
|
+
use_fts5=has_fts5_table(_db_conn),
|
|
544
|
+
min_confidence=float(min_confidence),
|
|
545
|
+
since_days=int(since_days) if since_days and since_days > 0 else None,
|
|
546
|
+
)
|
|
547
|
+
# Log each returned row for the outcome-feedback loop.
|
|
548
|
+
rows = (result.get("results") or []) if isinstance(result, dict) else []
|
|
549
|
+
rids = log_retrieval(_db_conn, layer="memory", query=query, rows=rows)
|
|
550
|
+
if isinstance(result, dict):
|
|
551
|
+
result["retrieval_ids"] = rids
|
|
552
|
+
# Router-level telemetry.
|
|
553
|
+
log_router_decision(_db_conn, query=query, chosen_layer="memory", bytes_returned=len(str(rows)))
|
|
554
|
+
# A real Orient memory query — record the marker enforce-memory-check reads,
|
|
555
|
+
# so the honest path is automatic and the marker means an actual search ran.
|
|
556
|
+
_record_memory_check_safe(query)
|
|
557
|
+
return ok(
|
|
558
|
+
result,
|
|
559
|
+
meta={
|
|
560
|
+
"layer": "memory",
|
|
561
|
+
"query": query,
|
|
562
|
+
"source": result.get("source") if isinstance(result, dict) else None,
|
|
563
|
+
},
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
@mcp.tool(
|
|
568
|
+
name="cos_timeline",
|
|
569
|
+
annotations={
|
|
570
|
+
"title": "Thinking OS Timeline",
|
|
571
|
+
"readOnlyHint": True,
|
|
572
|
+
"destructiveHint": False,
|
|
573
|
+
"idempotentHint": True,
|
|
574
|
+
"openWorldHint": False,
|
|
575
|
+
},
|
|
576
|
+
)
|
|
577
|
+
@safe_tool(name="cos_timeline")
|
|
578
|
+
def thinking_os_timeline(
|
|
579
|
+
days: int = 30,
|
|
580
|
+
domain: str = "",
|
|
581
|
+
limit: int = 20,
|
|
582
|
+
) -> str:
|
|
583
|
+
"""Get recent task outcomes and observations timeline.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
days: Lookback window (1-365, default 30).
|
|
587
|
+
domain: Filter by domain (e.g. "BACKEND"). Optional.
|
|
588
|
+
limit: Max entries (1-50, default 20).
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
str: JSON with timeline entries [{id, title, date, outcome, type}].
|
|
592
|
+
"""
|
|
593
|
+
result = memory_timeline(
|
|
594
|
+
_db_conn,
|
|
595
|
+
days=days,
|
|
596
|
+
domain=domain or None,
|
|
597
|
+
limit=limit,
|
|
598
|
+
)
|
|
599
|
+
return ok(
|
|
600
|
+
result,
|
|
601
|
+
meta={"layer": "memory", "filters_applied": {"domain": domain or None, "days": days}},
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
@mcp.tool(
|
|
606
|
+
name="cos_details",
|
|
607
|
+
annotations={
|
|
608
|
+
"title": "Thinking OS Details",
|
|
609
|
+
"readOnlyHint": False, # updates access_count
|
|
610
|
+
"destructiveHint": False,
|
|
611
|
+
"idempotentHint": False,
|
|
612
|
+
"openWorldHint": False,
|
|
613
|
+
},
|
|
614
|
+
)
|
|
615
|
+
@safe_tool(name="cos_details")
|
|
616
|
+
def thinking_os_details(
|
|
617
|
+
pattern_id: int,
|
|
618
|
+
source: str = "learned_patterns",
|
|
619
|
+
) -> str:
|
|
620
|
+
"""Get full details of a pattern, observation, or task outcome.
|
|
621
|
+
|
|
622
|
+
Args:
|
|
623
|
+
pattern_id: Row ID (or task_id string for task_outcomes).
|
|
624
|
+
source: Table name — observations, learned_patterns, or task_outcomes.
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
str: JSON with full record.
|
|
628
|
+
"""
|
|
629
|
+
result = memory_details(
|
|
630
|
+
_db_conn,
|
|
631
|
+
pattern_id=pattern_id,
|
|
632
|
+
source=source,
|
|
633
|
+
)
|
|
634
|
+
return ok(result, meta={"layer": "memory"})
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
@mcp.tool(
|
|
638
|
+
name="cos_promote",
|
|
639
|
+
annotations={
|
|
640
|
+
"title": "Promote Pattern to Rule",
|
|
641
|
+
"readOnlyHint": False,
|
|
642
|
+
"destructiveHint": False,
|
|
643
|
+
"idempotentHint": True,
|
|
644
|
+
"openWorldHint": False,
|
|
645
|
+
},
|
|
646
|
+
)
|
|
647
|
+
@safe_tool
|
|
648
|
+
def thinking_os_promote_tool(
|
|
649
|
+
pattern_id: int,
|
|
650
|
+
target: str = "feedback",
|
|
651
|
+
) -> str:
|
|
652
|
+
"""Promote a validated pattern to a rule or feedback memory file.
|
|
653
|
+
|
|
654
|
+
Requires confidence >= 0.3. Creates file content but does NOT write to disk
|
|
655
|
+
(caller writes the returned content to the appropriate location).
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
pattern_id: ID in learned_patterns table.
|
|
659
|
+
target: Output type — "feedback" or "rule".
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
str: JSON with status, filename, and file content to write.
|
|
663
|
+
"""
|
|
664
|
+
result = memory_promote(
|
|
665
|
+
_db_conn,
|
|
666
|
+
pattern_id=pattern_id,
|
|
667
|
+
target=target,
|
|
668
|
+
memory_dir="", # caller handles file writing
|
|
669
|
+
)
|
|
670
|
+
return ok(result, meta={"layer": "memory"})
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
# ---------------------------------------------------------------------------
|
|
674
|
+
# Learning tools
|
|
675
|
+
# ---------------------------------------------------------------------------
|
|
676
|
+
def _panel_or_agent_dir() -> str | None:
|
|
677
|
+
# The per-session state dir a marker belongs in, most-specific first:
|
|
678
|
+
# COS_PANEL_DIR (the reminder + session-context reset both target it), then
|
|
679
|
+
# COS_AGENT_DIR, then <state>/<agent> (COS_AGENT env or the .agent marker).
|
|
680
|
+
import os as _os
|
|
681
|
+
from pathlib import Path as _P
|
|
682
|
+
|
|
683
|
+
target_dir = _os.environ.get("COS_PANEL_DIR") or _os.environ.get("COS_AGENT_DIR")
|
|
684
|
+
if target_dir:
|
|
685
|
+
return target_dir
|
|
686
|
+
state_dir = _P(_os.environ.get("COS_STATE_DIR", ".coding-os"))
|
|
687
|
+
agent = _os.environ.get("COS_AGENT", "")
|
|
688
|
+
if not agent:
|
|
689
|
+
marker = state_dir / ".agent"
|
|
690
|
+
if marker.exists():
|
|
691
|
+
agent = marker.read_text(encoding="utf-8").strip()
|
|
692
|
+
return str(state_dir / agent) if agent else None
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _persist_learn_suggestions_safe(result: dict) -> None:
|
|
696
|
+
"""Append surfaced pattern ids to the panel-dir .learn-suggestions."""
|
|
697
|
+
try:
|
|
698
|
+
from pathlib import Path as _P
|
|
699
|
+
|
|
700
|
+
# Panel dir first: the same file auto_compose.py writes, the task-done
|
|
701
|
+
# reminder reads, and session-context.sh resets. The old COS_AGENT_DIR
|
|
702
|
+
# target was a file nothing read and nothing pruned.
|
|
703
|
+
target_dir = _panel_or_agent_dir()
|
|
704
|
+
if not target_dir:
|
|
705
|
+
return
|
|
706
|
+
suggestions = (result or {}).get("suggestions") or []
|
|
707
|
+
if not suggestions:
|
|
708
|
+
return
|
|
709
|
+
target = _P(target_dir) / ".learn-suggestions"
|
|
710
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
711
|
+
lines: list[str] = []
|
|
712
|
+
for s in suggestions:
|
|
713
|
+
if not isinstance(s, dict):
|
|
714
|
+
continue
|
|
715
|
+
pid = s.get("id")
|
|
716
|
+
txt = (s.get("pattern") or "").replace("\t", " ").replace("\n", " ")
|
|
717
|
+
if pid is None:
|
|
718
|
+
continue
|
|
719
|
+
lines.append(f"{pid}\t{txt}")
|
|
720
|
+
if lines:
|
|
721
|
+
with target.open("a", encoding="utf-8") as f:
|
|
722
|
+
f.write("\n".join(lines) + "\n")
|
|
723
|
+
except Exception as exc:
|
|
724
|
+
logger.debug("_persist_learn_suggestions_safe swallowed: %s", exc)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _record_memory_check_safe(query: str) -> None:
|
|
728
|
+
"""Mark the Orient memory-check satisfied by a REAL cos_search, so
|
|
729
|
+
enforce-memory-check reflects an actual query, not a self-attested claim."""
|
|
730
|
+
try:
|
|
731
|
+
from pathlib import Path as _P
|
|
732
|
+
|
|
733
|
+
target_dir = _panel_or_agent_dir()
|
|
734
|
+
if not target_dir:
|
|
735
|
+
return
|
|
736
|
+
marker = _P(target_dir) / ".memory-check"
|
|
737
|
+
marker.parent.mkdir(parents=True, exist_ok=True)
|
|
738
|
+
marker.write_text(f"cos_search:{(query or '')[:120]}\n", encoding="utf-8")
|
|
739
|
+
except Exception as exc:
|
|
740
|
+
logger.debug("_record_memory_check_safe swallowed: %s", exc)
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
@mcp.tool(
|
|
744
|
+
name="cos_learn_extract",
|
|
745
|
+
annotations={
|
|
746
|
+
"title": "Extract Learned Patterns",
|
|
747
|
+
"readOnlyHint": False,
|
|
748
|
+
"destructiveHint": False,
|
|
749
|
+
"idempotentHint": True,
|
|
750
|
+
"openWorldHint": False,
|
|
751
|
+
},
|
|
752
|
+
)
|
|
753
|
+
@safe_tool
|
|
754
|
+
def cos_learn_extract(min_occurrences: int = 3) -> str:
|
|
755
|
+
"""Scan task outcomes to discover recurring patterns.
|
|
756
|
+
|
|
757
|
+
Detects domain_rework, skill_correlation, and complexity_mismatch patterns.
|
|
758
|
+
Inserts new patterns into learned_patterns with calculated confidence.
|
|
759
|
+
|
|
760
|
+
Args:
|
|
761
|
+
min_occurrences: Minimum occurrences to consider a pattern (default 3).
|
|
762
|
+
|
|
763
|
+
Returns:
|
|
764
|
+
str: JSON with extracted patterns list and analysis stats.
|
|
765
|
+
"""
|
|
766
|
+
result = learn_extract(_db_conn, min_occurrences=min_occurrences)
|
|
767
|
+
return ok(result, meta={"layer": "learning"})
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
@mcp.tool(
|
|
771
|
+
name="cos_learn_suggest",
|
|
772
|
+
annotations={
|
|
773
|
+
"title": "Suggest Learned Patterns",
|
|
774
|
+
"readOnlyHint": True,
|
|
775
|
+
"destructiveHint": False,
|
|
776
|
+
"idempotentHint": True,
|
|
777
|
+
"openWorldHint": False,
|
|
778
|
+
},
|
|
779
|
+
)
|
|
780
|
+
@safe_tool
|
|
781
|
+
def cos_learn_suggest(
|
|
782
|
+
domain: str = "",
|
|
783
|
+
complexity: str = "",
|
|
784
|
+
task_type: str = "",
|
|
785
|
+
limit: int = 5,
|
|
786
|
+
) -> str:
|
|
787
|
+
"""Return relevant patterns for the current task context.
|
|
788
|
+
|
|
789
|
+
Includes spaced repetition: fading patterns (0.2-0.4 confidence) that
|
|
790
|
+
were once validated get priority for re-validation.
|
|
791
|
+
|
|
792
|
+
Args:
|
|
793
|
+
domain: Task domain (e.g. "BACKEND"). Optional.
|
|
794
|
+
complexity: Cynefin classification. Optional.
|
|
795
|
+
task_type: Type of task (e.g. "feat"). Optional.
|
|
796
|
+
limit: Max suggestions (1-20, default 5).
|
|
797
|
+
|
|
798
|
+
Returns:
|
|
799
|
+
str: JSON with suggestions list [{id, pattern, confidence, reason}].
|
|
800
|
+
"""
|
|
801
|
+
result = learn_suggest(
|
|
802
|
+
_db_conn,
|
|
803
|
+
domain=domain or None,
|
|
804
|
+
complexity=complexity or None,
|
|
805
|
+
task_type=task_type or None,
|
|
806
|
+
limit=limit,
|
|
807
|
+
)
|
|
808
|
+
# Persist the suggestion set so remind-learn-validate.sh
|
|
809
|
+
# can prompt the agent to close the loop after task-done. One line
|
|
810
|
+
# per pattern, format "id<TAB>text" — the hook prints a slice.
|
|
811
|
+
_persist_learn_suggestions_safe(result)
|
|
812
|
+
return ok(
|
|
813
|
+
result,
|
|
814
|
+
meta={
|
|
815
|
+
"layer": "learning",
|
|
816
|
+
"filters_applied": {
|
|
817
|
+
"domain": domain or None,
|
|
818
|
+
"complexity": complexity or None,
|
|
819
|
+
"task_type": task_type or None,
|
|
820
|
+
},
|
|
821
|
+
},
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
@mcp.tool(
|
|
826
|
+
name="cos_learn_validate",
|
|
827
|
+
annotations={
|
|
828
|
+
"title": "Validate Learned Pattern",
|
|
829
|
+
"readOnlyHint": False,
|
|
830
|
+
"destructiveHint": False,
|
|
831
|
+
"idempotentHint": False,
|
|
832
|
+
"openWorldHint": False,
|
|
833
|
+
},
|
|
834
|
+
)
|
|
835
|
+
@safe_tool
|
|
836
|
+
def cos_learn_validate(pattern_id: int, was_helpful: bool = True) -> str:
|
|
837
|
+
"""Record whether a suggested pattern was helpful.
|
|
838
|
+
|
|
839
|
+
Updates confidence using brain-inspired formulas:
|
|
840
|
+
- Helpful: LTP with diminishing returns + temporal proximity bonus
|
|
841
|
+
- Not helpful: LTD proportional penalty
|
|
842
|
+
|
|
843
|
+
Args:
|
|
844
|
+
pattern_id: ID in learned_patterns table.
|
|
845
|
+
was_helpful: Whether the pattern was useful (default True).
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
str: JSON with old/new confidence and validation status.
|
|
849
|
+
"""
|
|
850
|
+
result = learn_validate(_db_conn, pattern_id=pattern_id, was_helpful=was_helpful)
|
|
851
|
+
return ok(result, meta={"layer": "learning"})
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
@mcp.tool(
|
|
855
|
+
name="cos_learn_narrative",
|
|
856
|
+
annotations={
|
|
857
|
+
"title": "Record Breakthrough Narrative",
|
|
858
|
+
"readOnlyHint": False,
|
|
859
|
+
"destructiveHint": False,
|
|
860
|
+
"idempotentHint": False,
|
|
861
|
+
"openWorldHint": False,
|
|
862
|
+
},
|
|
863
|
+
)
|
|
864
|
+
@safe_tool
|
|
865
|
+
def cos_learn_narrative(
|
|
866
|
+
task_id: str,
|
|
867
|
+
what_failed: str = "",
|
|
868
|
+
what_worked: str = "",
|
|
869
|
+
key_insight: str = "",
|
|
870
|
+
) -> str:
|
|
871
|
+
"""Record what was learned from a difficult task (breakthrough narrative).
|
|
872
|
+
|
|
873
|
+
Call this after a rework→success breakthrough to capture:
|
|
874
|
+
- What approaches failed and why
|
|
875
|
+
- What finally worked
|
|
876
|
+
- The reusable key insight
|
|
877
|
+
|
|
878
|
+
Creates a high-impact learned pattern for future suggestions.
|
|
879
|
+
|
|
880
|
+
Args:
|
|
881
|
+
task_id: Task identifier (e.g. "TASK-100").
|
|
882
|
+
what_failed: Approaches that didn't work.
|
|
883
|
+
what_worked: The solution that resolved the issue.
|
|
884
|
+
key_insight: Reusable lesson learned (required).
|
|
885
|
+
|
|
886
|
+
Returns:
|
|
887
|
+
str: JSON with status, history_id, pattern_id.
|
|
888
|
+
"""
|
|
889
|
+
result = learn_narrative(
|
|
890
|
+
_db_conn,
|
|
891
|
+
task_id=task_id,
|
|
892
|
+
what_failed=what_failed,
|
|
893
|
+
what_worked=what_worked,
|
|
894
|
+
key_insight=key_insight,
|
|
895
|
+
)
|
|
896
|
+
return ok(result, meta={"layer": "learning"})
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
# ---------------------------------------------------------------------------
|
|
900
|
+
# Graph tools (v4 brain features)
|
|
901
|
+
# ---------------------------------------------------------------------------
|
|
902
|
+
# W7.10 / R4-14: legacy cos_graph stub removed entirely. Use
|
|
903
|
+
# cos_graph_resolve(q) → cos_graph_context(uid) / cos_graph_impact(uid)
|
|
904
|
+
# / cos_graph_references(uid) instead.
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
# ---------------------------------------------------------------------------
|
|
908
|
+
# Routing tools
|
|
909
|
+
# ---------------------------------------------------------------------------
|
|
910
|
+
@mcp.tool(
|
|
911
|
+
name="cos_route_model",
|
|
912
|
+
annotations={
|
|
913
|
+
"title": "Route Model Recommendation",
|
|
914
|
+
"readOnlyHint": True,
|
|
915
|
+
"destructiveHint": False,
|
|
916
|
+
"idempotentHint": True,
|
|
917
|
+
"openWorldHint": False,
|
|
918
|
+
},
|
|
919
|
+
)
|
|
920
|
+
@safe_tool
|
|
921
|
+
def cos_route_model(
|
|
922
|
+
complexity: str,
|
|
923
|
+
dimensions: int = 1,
|
|
924
|
+
domain: str = "",
|
|
925
|
+
) -> str:
|
|
926
|
+
"""Recommend optimal model based on historical outcome data.
|
|
927
|
+
|
|
928
|
+
Cold start (<10 outcomes): returns static default from performance.md.
|
|
929
|
+
Warm: queries success rates per model for the given complexity+domain.
|
|
930
|
+
|
|
931
|
+
Args:
|
|
932
|
+
complexity: Cynefin classification (CLEAR/COMPLICATED/COMPLEX/CHAOTIC).
|
|
933
|
+
dimensions: Number of problem dimensions (default 1).
|
|
934
|
+
domain: Task domain (e.g. "BACKEND"). Optional.
|
|
935
|
+
|
|
936
|
+
Returns:
|
|
937
|
+
str: JSON with recommended_model, confidence, reason, fallback_model.
|
|
938
|
+
"""
|
|
939
|
+
result = route_model_bandit(
|
|
940
|
+
_db_conn,
|
|
941
|
+
complexity=complexity,
|
|
942
|
+
dimensions=dimensions,
|
|
943
|
+
domain=domain or None,
|
|
944
|
+
)
|
|
945
|
+
return ok(result, meta={"layer": "routing"})
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
@mcp.tool(
|
|
949
|
+
name="cos_route_skill",
|
|
950
|
+
annotations={
|
|
951
|
+
"title": "Route Skill Recommendation",
|
|
952
|
+
"readOnlyHint": True,
|
|
953
|
+
"destructiveHint": False,
|
|
954
|
+
"idempotentHint": True,
|
|
955
|
+
"openWorldHint": False,
|
|
956
|
+
},
|
|
957
|
+
)
|
|
958
|
+
@safe_tool
|
|
959
|
+
def cos_route_skill(
|
|
960
|
+
domain: str,
|
|
961
|
+
task_type: str = "",
|
|
962
|
+
complexity: str = "",
|
|
963
|
+
) -> str:
|
|
964
|
+
"""Recommend skills based on historical outcome data.
|
|
965
|
+
|
|
966
|
+
Cold start: returns static defaults from skill-enforcement.md.
|
|
967
|
+
Warm: augments with historically successful skills.
|
|
968
|
+
|
|
969
|
+
Args:
|
|
970
|
+
domain: Task domain (e.g. "BACKEND", "FRONTEND").
|
|
971
|
+
task_type: Type of task (e.g. "feat", "fix"). Optional.
|
|
972
|
+
complexity: Cynefin classification. Optional.
|
|
973
|
+
|
|
974
|
+
Returns:
|
|
975
|
+
str: JSON with skills list [{name, confidence, reason}].
|
|
976
|
+
"""
|
|
977
|
+
result = route_skill(
|
|
978
|
+
_db_conn,
|
|
979
|
+
domain=domain,
|
|
980
|
+
task_type=task_type or None,
|
|
981
|
+
complexity=complexity or None,
|
|
982
|
+
)
|
|
983
|
+
return ok(result, meta={"layer": "routing"})
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
# ---------------------------------------------------------------------------
|
|
987
|
+
# Project Trajectory + Failure Archaeology + Routing Drift
|
|
988
|
+
# ---------------------------------------------------------------------------
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
@mcp.tool(
|
|
992
|
+
name="cos_trajectory_snapshot",
|
|
993
|
+
annotations={
|
|
994
|
+
"title": "Project Trajectory Snapshot (Write)",
|
|
995
|
+
"readOnlyHint": False,
|
|
996
|
+
"destructiveHint": False,
|
|
997
|
+
"idempotentHint": False,
|
|
998
|
+
"openWorldHint": False,
|
|
999
|
+
},
|
|
1000
|
+
)
|
|
1001
|
+
@safe_tool
|
|
1002
|
+
def cos_trajectory_snapshot(
|
|
1003
|
+
session_id: str,
|
|
1004
|
+
phase: str = "",
|
|
1005
|
+
current_focus: str = "",
|
|
1006
|
+
architectural_decisions: str = "[]",
|
|
1007
|
+
anti_patterns_discovered: str = "[]",
|
|
1008
|
+
open_questions: str = "[]",
|
|
1009
|
+
next_logical_step: str = "",
|
|
1010
|
+
confidence: float = 0.7,
|
|
1011
|
+
) -> str:
|
|
1012
|
+
"""Persist a project trajectory snapshot for the current session.
|
|
1013
|
+
|
|
1014
|
+
Records WHERE the project is heading (phase, focus, architectural decisions,
|
|
1015
|
+
anti-patterns discovered, open questions) so future sessions have strategic
|
|
1016
|
+
context beyond task history. Each call creates a new row linked to the
|
|
1017
|
+
previous snapshot via supersedes_id.
|
|
1018
|
+
|
|
1019
|
+
Args:
|
|
1020
|
+
session_id: Current session identifier.
|
|
1021
|
+
phase: Current development phase (e.g. "v2 hardening").
|
|
1022
|
+
current_focus: What the team is focused on right now.
|
|
1023
|
+
architectural_decisions: JSON array of {decision, rationale} objects.
|
|
1024
|
+
anti_patterns_discovered: JSON array of {pattern, context} objects.
|
|
1025
|
+
open_questions: JSON array of {question, priority} objects or plain strings.
|
|
1026
|
+
next_logical_step: Single-sentence description of what comes next.
|
|
1027
|
+
confidence: Confidence in this trajectory assessment (0.0-1.0).
|
|
1028
|
+
|
|
1029
|
+
Returns:
|
|
1030
|
+
JSON with {status, id, supersedes_id}.
|
|
1031
|
+
"""
|
|
1032
|
+
import json as _json
|
|
1033
|
+
|
|
1034
|
+
try:
|
|
1035
|
+
ad = _json.loads(architectural_decisions or "[]")
|
|
1036
|
+
apd = _json.loads(anti_patterns_discovered or "[]")
|
|
1037
|
+
oq = _json.loads(open_questions or "[]")
|
|
1038
|
+
except _json.JSONDecodeError as exc:
|
|
1039
|
+
return fail("validation", f"JSON parse error in list field: {exc}")
|
|
1040
|
+
|
|
1041
|
+
result = trajectory_snapshot(
|
|
1042
|
+
_db_conn,
|
|
1043
|
+
session_id=session_id,
|
|
1044
|
+
phase=phase,
|
|
1045
|
+
current_focus=current_focus,
|
|
1046
|
+
architectural_decisions=ad,
|
|
1047
|
+
anti_patterns_discovered=apd,
|
|
1048
|
+
open_questions=oq,
|
|
1049
|
+
next_logical_step=next_logical_step,
|
|
1050
|
+
confidence=confidence,
|
|
1051
|
+
)
|
|
1052
|
+
return ok(result, meta={"layer": "trajectory"})
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
@mcp.tool(
|
|
1056
|
+
name="cos_trajectory_read",
|
|
1057
|
+
annotations={
|
|
1058
|
+
"title": "Project Trajectory Read",
|
|
1059
|
+
"readOnlyHint": True,
|
|
1060
|
+
"destructiveHint": False,
|
|
1061
|
+
"idempotentHint": True,
|
|
1062
|
+
"openWorldHint": False,
|
|
1063
|
+
},
|
|
1064
|
+
)
|
|
1065
|
+
@safe_tool
|
|
1066
|
+
def cos_trajectory_read(limit: int = 1) -> str:
|
|
1067
|
+
"""Return the most recent project trajectory snapshot(s).
|
|
1068
|
+
|
|
1069
|
+
Use at session start to understand WHERE the project is heading before
|
|
1070
|
+
looking at the task board. Returns phase, current focus, architectural
|
|
1071
|
+
decisions made, anti-patterns discovered, and open questions.
|
|
1072
|
+
|
|
1073
|
+
Args:
|
|
1074
|
+
limit: Number of recent snapshots to return (1-20, default 1).
|
|
1075
|
+
|
|
1076
|
+
Returns:
|
|
1077
|
+
JSON with {snapshots: [...], count: int}.
|
|
1078
|
+
"""
|
|
1079
|
+
result = trajectory_read(_db_conn, limit=limit)
|
|
1080
|
+
return ok(result, meta={"layer": "trajectory"})
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
@mcp.tool(
|
|
1084
|
+
name="cos_failure_pattern_query",
|
|
1085
|
+
annotations={
|
|
1086
|
+
"title": "Failure Pattern Query",
|
|
1087
|
+
"readOnlyHint": True,
|
|
1088
|
+
"destructiveHint": False,
|
|
1089
|
+
"idempotentHint": True,
|
|
1090
|
+
"openWorldHint": False,
|
|
1091
|
+
},
|
|
1092
|
+
)
|
|
1093
|
+
@safe_tool
|
|
1094
|
+
def cos_failure_pattern_query(
|
|
1095
|
+
root_cause: str = "",
|
|
1096
|
+
domain: str = "",
|
|
1097
|
+
limit: int = 10,
|
|
1098
|
+
) -> str:
|
|
1099
|
+
"""Aggregate structured failure anatomy from backtrack_events.
|
|
1100
|
+
|
|
1101
|
+
Returns which root_cause categories recur most frequently, with examples.
|
|
1102
|
+
Use before planning to avoid known failure modes. Requires migration v25
|
|
1103
|
+
(structured backtrack anatomy columns).
|
|
1104
|
+
|
|
1105
|
+
root_cause filter values: wrong_model | scope_too_large | missing_context |
|
|
1106
|
+
tool_failure | spec_ambiguity | env_mismatch | other
|
|
1107
|
+
|
|
1108
|
+
Args:
|
|
1109
|
+
root_cause: Optional filter to a specific root cause category.
|
|
1110
|
+
domain: Reserved for future per-domain filtering.
|
|
1111
|
+
limit: Max pattern groups to return (1-50, default 10).
|
|
1112
|
+
|
|
1113
|
+
Returns:
|
|
1114
|
+
JSON with {patterns: [{root_cause, count, examples}],
|
|
1115
|
+
total_structured, total_backtrack}.
|
|
1116
|
+
"""
|
|
1117
|
+
result = failure_pattern_query(
|
|
1118
|
+
_db_conn,
|
|
1119
|
+
root_cause=root_cause or None,
|
|
1120
|
+
domain=domain or None,
|
|
1121
|
+
limit=limit,
|
|
1122
|
+
)
|
|
1123
|
+
return ok(result, meta={"layer": "routing"})
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
# ---------------------------------------------------------------------------
|
|
1127
|
+
# Document RAG search
|
|
1128
|
+
# ---------------------------------------------------------------------------
|
|
1129
|
+
@mcp.tool(
|
|
1130
|
+
name="cos_doc_search",
|
|
1131
|
+
annotations={
|
|
1132
|
+
"title": "Search Project Documentation",
|
|
1133
|
+
"readOnlyHint": True,
|
|
1134
|
+
"destructiveHint": False,
|
|
1135
|
+
"idempotentHint": True,
|
|
1136
|
+
"openWorldHint": False,
|
|
1137
|
+
},
|
|
1138
|
+
)
|
|
1139
|
+
@safe_tool
|
|
1140
|
+
def cos_doc_search(
|
|
1141
|
+
query: str,
|
|
1142
|
+
source_types: str = "",
|
|
1143
|
+
limit: int = 5,
|
|
1144
|
+
mode: str = "auto",
|
|
1145
|
+
domain: str = "",
|
|
1146
|
+
layer: str = "",
|
|
1147
|
+
since_iso: str = "",
|
|
1148
|
+
include_inactive: bool = False,
|
|
1149
|
+
auto_context: bool = True,
|
|
1150
|
+
) -> str:
|
|
1151
|
+
"""Semantic + lexical search over project documentation chunks.
|
|
1152
|
+
|
|
1153
|
+
Stage-1 metadata pre-filter (since migration v22):
|
|
1154
|
+
`domain`, `layer`, `since_iso`, and `include_inactive` narrow the
|
|
1155
|
+
chunk universe BEFORE vector / FTS ranking. Vector search finds
|
|
1156
|
+
meaning; metadata enforces reality (correct era, correct domain,
|
|
1157
|
+
not superseded). Combine with `source_types` for cheap, indexed
|
|
1158
|
+
pre-filtering.
|
|
1159
|
+
|
|
1160
|
+
Args:
|
|
1161
|
+
query: Natural language search query (e.g. "commission rate calculation").
|
|
1162
|
+
source_types: Optional comma-separated filter — restrict to specific
|
|
1163
|
+
source types (e.g. "prd,architecture,adr"). Empty = all types.
|
|
1164
|
+
limit: Maximum results (1-50, default 5).
|
|
1165
|
+
mode: "auto" (default) | "semantic" | "lexical".
|
|
1166
|
+
domain: Frontmatter `domain:` filter (BACKEND, FRONTEND, OPS,
|
|
1167
|
+
DOCS, …). Empty = any. Indexed.
|
|
1168
|
+
layer: Frontmatter `layer:` filter (adr, playbook, spec, policy,
|
|
1169
|
+
reference, runbook, postmortem, task). Empty = any. Indexed.
|
|
1170
|
+
since_iso: Lower bound on frontmatter `updated:` (YYYY-MM-DD).
|
|
1171
|
+
Use when the agent asks about "recent" or "current" state and
|
|
1172
|
+
a stale older doc would be the wrong answer. Empty = any age.
|
|
1173
|
+
include_inactive: When False (default), hide chunks marked
|
|
1174
|
+
is_active=0 because the source doc was deleted or superseded.
|
|
1175
|
+
Set True for decision-history retrieval that must surface
|
|
1176
|
+
superseded specs.
|
|
1177
|
+
auto_context: When True (default), soft-default `domain` from the
|
|
1178
|
+
active task's swimlane ($COS_AGENT_DIR/.swimlane). Explicit
|
|
1179
|
+
`domain` argument always wins. Set False to disable.
|
|
1180
|
+
|
|
1181
|
+
Response meta carries `filter_hints` — heuristic suggestions
|
|
1182
|
+
extracted from the query (date phrasing, domain keywords, layer
|
|
1183
|
+
cues). Suggestions are NEVER auto-applied; the agent decides
|
|
1184
|
+
whether to re-query with them. Mental model: Filter → Search →
|
|
1185
|
+
Summarize. Vector finds meaning, metadata enforces correctness.
|
|
1186
|
+
|
|
1187
|
+
Returns:
|
|
1188
|
+
str: JSON envelope with results list and count. Each result
|
|
1189
|
+
carries source_path, source_type, heading_path, content,
|
|
1190
|
+
score, priority, mtime, chunk_index, retrieval_source.
|
|
1191
|
+
"""
|
|
1192
|
+
types = [t.strip() for t in source_types.split(",") if t.strip()] or None
|
|
1193
|
+
mode_clean = mode if mode in ("auto", "semantic", "lexical") else "auto"
|
|
1194
|
+
domain_clean = domain.strip() or None
|
|
1195
|
+
layer_clean = layer.strip() or None
|
|
1196
|
+
since_clean = since_iso.strip() or None
|
|
1197
|
+
|
|
1198
|
+
results, search_meta = doc_search(
|
|
1199
|
+
_db_conn,
|
|
1200
|
+
query=query,
|
|
1201
|
+
source_types=types,
|
|
1202
|
+
limit=limit,
|
|
1203
|
+
mode=mode_clean,
|
|
1204
|
+
domain=domain_clean,
|
|
1205
|
+
layer=layer_clean,
|
|
1206
|
+
since_iso=since_clean,
|
|
1207
|
+
include_inactive=include_inactive,
|
|
1208
|
+
auto_context=auto_context,
|
|
1209
|
+
return_meta=True,
|
|
1210
|
+
)
|
|
1211
|
+
# Derive retrieval source from result rows for diagnostic meta.
|
|
1212
|
+
if results:
|
|
1213
|
+
sources_used = sorted(
|
|
1214
|
+
{r.get("retrieval_source") for r in results if r.get("retrieval_source")}
|
|
1215
|
+
)
|
|
1216
|
+
source_label = "+".join(sources_used) if sources_used else mode_clean
|
|
1217
|
+
else:
|
|
1218
|
+
source_label = "empty"
|
|
1219
|
+
# Outcome-feedback loop logging.
|
|
1220
|
+
rids = log_retrieval(_db_conn, layer="docs", query=query, rows=results)
|
|
1221
|
+
# Router-level telemetry.
|
|
1222
|
+
log_router_decision(
|
|
1223
|
+
_db_conn, query=query, chosen_layer="docs", bytes_returned=len(str(results))
|
|
1224
|
+
)
|
|
1225
|
+
# D7-F4: when the rag embedding extra is unavailable, retrieval
|
|
1226
|
+
# silently degrades to FTS-only — surface that as retrieval_mode so the
|
|
1227
|
+
# beginner persona is warned, not misled. An explicit lexical request keeps
|
|
1228
|
+
# its own mode (intentional, not a degradation).
|
|
1229
|
+
from embeddings import is_available as _emb_available
|
|
1230
|
+
|
|
1231
|
+
retrieval_mode = mode_clean if _emb_available() else "lexical-only"
|
|
1232
|
+
return ok(
|
|
1233
|
+
{"results": results, "count": len(results), "retrieval_ids": rids},
|
|
1234
|
+
meta={
|
|
1235
|
+
"layer": "docs",
|
|
1236
|
+
"query": query,
|
|
1237
|
+
"mode": mode_clean,
|
|
1238
|
+
"retrieval_mode": retrieval_mode,
|
|
1239
|
+
"source": source_label,
|
|
1240
|
+
"filters_applied": search_meta.get("applied", {}),
|
|
1241
|
+
"filter_hints": search_meta.get("filter_hints", {}),
|
|
1242
|
+
},
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
# ---------------------------------------------------------------------------
|
|
1247
|
+
# Doc header tools: header-only lazy load
|
|
1248
|
+
# ---------------------------------------------------------------------------
|
|
1249
|
+
@mcp.tool(
|
|
1250
|
+
name="cos_doc_header",
|
|
1251
|
+
annotations={
|
|
1252
|
+
"title": "Read Doc Header (frontmatter + opening block)",
|
|
1253
|
+
"readOnlyHint": True,
|
|
1254
|
+
"destructiveHint": False,
|
|
1255
|
+
"idempotentHint": True,
|
|
1256
|
+
"openWorldHint": False,
|
|
1257
|
+
},
|
|
1258
|
+
)
|
|
1259
|
+
@safe_tool
|
|
1260
|
+
def cos_doc_header(path: str) -> str:
|
|
1261
|
+
"""Return a single doc's header without reading the body."""
|
|
1262
|
+
candidate = (path or "").strip()
|
|
1263
|
+
if not candidate:
|
|
1264
|
+
return fail("validation", "path is required")
|
|
1265
|
+
root_dir = project_root().resolve()
|
|
1266
|
+
target = Path(candidate)
|
|
1267
|
+
if not target.is_absolute():
|
|
1268
|
+
target = (root_dir / target).resolve()
|
|
1269
|
+
else:
|
|
1270
|
+
try:
|
|
1271
|
+
target = target.resolve()
|
|
1272
|
+
except OSError as exc:
|
|
1273
|
+
return fail("validation", f"cannot resolve path: {exc}")
|
|
1274
|
+
# Path-traversal guard. The MCP server is trusted today,
|
|
1275
|
+
# but a future external client must never read files outside the
|
|
1276
|
+
# project root via this tool.
|
|
1277
|
+
try:
|
|
1278
|
+
target.relative_to(root_dir)
|
|
1279
|
+
except ValueError:
|
|
1280
|
+
return fail(
|
|
1281
|
+
"permission",
|
|
1282
|
+
f"path escapes project root: {candidate}",
|
|
1283
|
+
)
|
|
1284
|
+
if not target.exists():
|
|
1285
|
+
return fail("not_found", f"no such file: {candidate}")
|
|
1286
|
+
header = parse_doc_header(target)
|
|
1287
|
+
if header is None:
|
|
1288
|
+
return fail("validation", f"cannot parse doc header: {candidate}")
|
|
1289
|
+
return ok(
|
|
1290
|
+
header,
|
|
1291
|
+
meta={"layer": "docs", "source": "filesystem", "query": candidate},
|
|
1292
|
+
)
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
@mcp.tool(
|
|
1296
|
+
name="cos_doc_headers_by",
|
|
1297
|
+
annotations={
|
|
1298
|
+
"title": "List Doc Headers by Frontmatter Filter",
|
|
1299
|
+
"readOnlyHint": True,
|
|
1300
|
+
"destructiveHint": False,
|
|
1301
|
+
"idempotentHint": True,
|
|
1302
|
+
"openWorldHint": False,
|
|
1303
|
+
},
|
|
1304
|
+
)
|
|
1305
|
+
@safe_tool
|
|
1306
|
+
def cos_doc_headers_by(
|
|
1307
|
+
domain: str = "",
|
|
1308
|
+
layer: str = "",
|
|
1309
|
+
ssot: str = "",
|
|
1310
|
+
since_iso: str = "",
|
|
1311
|
+
root: str = "docs",
|
|
1312
|
+
limit: int = 50,
|
|
1313
|
+
) -> str:
|
|
1314
|
+
"""Bulk header-only scan filtered by frontmatter."""
|
|
1315
|
+
cap = max(1, min(int(limit) if limit else 50, 200))
|
|
1316
|
+
root_dir = project_root().resolve()
|
|
1317
|
+
root_path = Path(root) if root else Path("docs")
|
|
1318
|
+
if not root_path.is_absolute():
|
|
1319
|
+
root_path = (root_dir / root_path).resolve()
|
|
1320
|
+
else:
|
|
1321
|
+
try:
|
|
1322
|
+
root_path = root_path.resolve()
|
|
1323
|
+
except OSError as exc:
|
|
1324
|
+
return fail("validation", f"cannot resolve root: {exc}")
|
|
1325
|
+
# Path-traversal guard — root must stay inside project.
|
|
1326
|
+
try:
|
|
1327
|
+
root_path.relative_to(root_dir)
|
|
1328
|
+
except ValueError:
|
|
1329
|
+
return fail("permission", f"root escapes project root: {root}")
|
|
1330
|
+
if not root_path.exists():
|
|
1331
|
+
return fail("not_found", f"no such root: {root}")
|
|
1332
|
+
rows = list_doc_headers(
|
|
1333
|
+
root_path,
|
|
1334
|
+
domain=domain or None,
|
|
1335
|
+
layer=layer or None,
|
|
1336
|
+
ssot=ssot or None,
|
|
1337
|
+
since_iso=since_iso or None,
|
|
1338
|
+
limit=cap,
|
|
1339
|
+
)
|
|
1340
|
+
return ok(
|
|
1341
|
+
{"results": rows, "count": len(rows)},
|
|
1342
|
+
meta={
|
|
1343
|
+
"layer": "docs",
|
|
1344
|
+
"source": "filesystem",
|
|
1345
|
+
"filters_applied": {
|
|
1346
|
+
k: v
|
|
1347
|
+
for k, v in {
|
|
1348
|
+
"domain": domain,
|
|
1349
|
+
"layer": layer,
|
|
1350
|
+
"ssot": ssot,
|
|
1351
|
+
"since_iso": since_iso,
|
|
1352
|
+
"root": str(root_path),
|
|
1353
|
+
}.items()
|
|
1354
|
+
if v
|
|
1355
|
+
},
|
|
1356
|
+
},
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
|
|
1360
|
+
# ---------------------------------------------------------------------------
|
|
1361
|
+
# Task store tools
|
|
1362
|
+
# ---------------------------------------------------------------------------
|
|
1363
|
+
@mcp.tool(
|
|
1364
|
+
name="cos_task_search",
|
|
1365
|
+
annotations={
|
|
1366
|
+
"title": "Search Tasks (Semantic + Filter)",
|
|
1367
|
+
"readOnlyHint": True,
|
|
1368
|
+
"destructiveHint": False,
|
|
1369
|
+
"idempotentHint": True,
|
|
1370
|
+
"openWorldHint": False,
|
|
1371
|
+
},
|
|
1372
|
+
)
|
|
1373
|
+
@safe_tool
|
|
1374
|
+
def cos_task_search(
|
|
1375
|
+
query: str,
|
|
1376
|
+
status: str = "",
|
|
1377
|
+
domain: str = "",
|
|
1378
|
+
limit: int = 10,
|
|
1379
|
+
) -> str:
|
|
1380
|
+
"""Semantic search over the task store with optional status/domain filters.
|
|
1381
|
+
|
|
1382
|
+
Use this when you need to find tasks related to a concept — even when
|
|
1383
|
+
exact keywords don't match. Falls back to LIKE on title + goal when
|
|
1384
|
+
embeddings are unavailable.
|
|
1385
|
+
|
|
1386
|
+
Args:
|
|
1387
|
+
query: Natural language query (e.g. "payment splitting multi vendor").
|
|
1388
|
+
status: Optional status filter — one of open/wip/done/blocked. Empty = all.
|
|
1389
|
+
domain: Optional domain filter (BACKEND/FRONTEND/DOCS/INFRA/...). Empty = all.
|
|
1390
|
+
limit: Maximum results (1-100, default 10).
|
|
1391
|
+
|
|
1392
|
+
Returns:
|
|
1393
|
+
JSON with results and count. Each result: task_id, title, domain,
|
|
1394
|
+
status, file_path, goal_text, dependencies, score.
|
|
1395
|
+
"""
|
|
1396
|
+
results = task_search(
|
|
1397
|
+
_db_conn,
|
|
1398
|
+
query=query,
|
|
1399
|
+
status=status or None,
|
|
1400
|
+
domain=domain or None,
|
|
1401
|
+
limit=limit,
|
|
1402
|
+
)
|
|
1403
|
+
rids = log_retrieval(_db_conn, layer="tasks", query=query, rows=results)
|
|
1404
|
+
log_router_decision(
|
|
1405
|
+
_db_conn, query=query, chosen_layer="tasks", bytes_returned=len(str(results))
|
|
1406
|
+
)
|
|
1407
|
+
return ok(
|
|
1408
|
+
{"results": results, "count": len(results), "retrieval_ids": rids},
|
|
1409
|
+
meta={
|
|
1410
|
+
"layer": "tasks",
|
|
1411
|
+
"query": query,
|
|
1412
|
+
"filters_applied": {"status": status or None, "domain": domain or None},
|
|
1413
|
+
},
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
@mcp.tool(
|
|
1418
|
+
name="cos_task_dependencies",
|
|
1419
|
+
annotations={
|
|
1420
|
+
"title": "Task Dependencies (Upstream)",
|
|
1421
|
+
"readOnlyHint": True,
|
|
1422
|
+
"destructiveHint": False,
|
|
1423
|
+
"idempotentHint": True,
|
|
1424
|
+
"openWorldHint": False,
|
|
1425
|
+
},
|
|
1426
|
+
)
|
|
1427
|
+
@safe_tool
|
|
1428
|
+
def cos_task_dependencies(task_id: str) -> str:
|
|
1429
|
+
"""Return the tasks that `task_id` directly depends on.
|
|
1430
|
+
|
|
1431
|
+
Use before starting a task to verify prerequisites are done. Returns
|
|
1432
|
+
only direct (first-level) dependencies — use repeated calls for
|
|
1433
|
+
transitive traversal.
|
|
1434
|
+
|
|
1435
|
+
Args:
|
|
1436
|
+
task_id: Task identifier (e.g. "TASK-199").
|
|
1437
|
+
|
|
1438
|
+
Returns:
|
|
1439
|
+
JSON with task_id, dependencies list, and count.
|
|
1440
|
+
"""
|
|
1441
|
+
results = task_dependencies(_db_conn, task_id)
|
|
1442
|
+
return ok(
|
|
1443
|
+
{"task_id": task_id, "dependencies": results, "count": len(results)},
|
|
1444
|
+
meta={"layer": "tasks"},
|
|
1445
|
+
)
|
|
1446
|
+
|
|
1447
|
+
|
|
1448
|
+
@mcp.tool(
|
|
1449
|
+
name="cos_task_dependents",
|
|
1450
|
+
annotations={
|
|
1451
|
+
"title": "Task Dependents (Downstream)",
|
|
1452
|
+
"readOnlyHint": True,
|
|
1453
|
+
"destructiveHint": False,
|
|
1454
|
+
"idempotentHint": True,
|
|
1455
|
+
"openWorldHint": False,
|
|
1456
|
+
},
|
|
1457
|
+
)
|
|
1458
|
+
@safe_tool
|
|
1459
|
+
def cos_task_dependents(task_id: str) -> str:
|
|
1460
|
+
"""Return the tasks that declare `task_id` as a dependency.
|
|
1461
|
+
|
|
1462
|
+
Use for impact analysis: "If I change TASK-195, what downstream tasks
|
|
1463
|
+
need to be re-verified?" Returns only direct dependents — non-transitive.
|
|
1464
|
+
|
|
1465
|
+
Args:
|
|
1466
|
+
task_id: Task identifier (e.g. "TASK-195").
|
|
1467
|
+
|
|
1468
|
+
Returns:
|
|
1469
|
+
JSON with task_id, dependents list, and count.
|
|
1470
|
+
"""
|
|
1471
|
+
results = task_dependents(_db_conn, task_id)
|
|
1472
|
+
return ok(
|
|
1473
|
+
{"task_id": task_id, "dependents": results, "count": len(results)}, meta={"layer": "tasks"}
|
|
1474
|
+
)
|
|
1475
|
+
|
|
1476
|
+
|
|
1477
|
+
@mcp.tool(
|
|
1478
|
+
name="cos_task_by_filter",
|
|
1479
|
+
annotations={
|
|
1480
|
+
"title": "List Tasks by Filter",
|
|
1481
|
+
"readOnlyHint": True,
|
|
1482
|
+
"destructiveHint": False,
|
|
1483
|
+
"idempotentHint": True,
|
|
1484
|
+
"openWorldHint": False,
|
|
1485
|
+
},
|
|
1486
|
+
)
|
|
1487
|
+
@safe_tool
|
|
1488
|
+
def cos_task_by_filter(
|
|
1489
|
+
status: str = "",
|
|
1490
|
+
domain: str = "",
|
|
1491
|
+
limit: int = 20,
|
|
1492
|
+
) -> str:
|
|
1493
|
+
"""List tasks matching an optional status and/or domain filter.
|
|
1494
|
+
|
|
1495
|
+
No semantic query — pure structured filter. Use when you need "all
|
|
1496
|
+
open backend tasks" or "all blocked tasks" without a specific concept.
|
|
1497
|
+
|
|
1498
|
+
Args:
|
|
1499
|
+
status: Filter by status (open/wip/done/blocked). Empty = all.
|
|
1500
|
+
domain: Filter by domain (BACKEND/FRONTEND/DOCS/...). Empty = all.
|
|
1501
|
+
limit: Maximum results (1-100, default 20).
|
|
1502
|
+
|
|
1503
|
+
Returns:
|
|
1504
|
+
JSON with results list (sorted by task_id ASC) and count.
|
|
1505
|
+
"""
|
|
1506
|
+
results = task_by_filter(
|
|
1507
|
+
_db_conn,
|
|
1508
|
+
status=status or None,
|
|
1509
|
+
domain=domain or None,
|
|
1510
|
+
limit=limit,
|
|
1511
|
+
)
|
|
1512
|
+
return ok(
|
|
1513
|
+
{"results": results, "count": len(results)},
|
|
1514
|
+
meta={
|
|
1515
|
+
"layer": "tasks",
|
|
1516
|
+
"filters_applied": {"status": status or None, "domain": domain or None},
|
|
1517
|
+
},
|
|
1518
|
+
)
|
|
1519
|
+
|
|
1520
|
+
|
|
1521
|
+
# ---------------------------------------------------------------------------
|
|
1522
|
+
# Board-OS MCP tools — Scrumban task board
|
|
1523
|
+
# ---------------------------------------------------------------------------
|
|
1524
|
+
# Imported from core/board_os/mcp_tools.py. Each tool here is a thin
|
|
1525
|
+
# @mcp.tool-decorated wrapper that injects the server's shared _db_conn.
|
|
1526
|
+
|
|
1527
|
+
try:
|
|
1528
|
+
# `from board_os...` requires the project root (parent of `core/`)
|
|
1529
|
+
# on sys.path, since `core/` is a namespace package without __init__.py.
|
|
1530
|
+
_PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
1531
|
+
if str(_PROJECT_ROOT) not in sys.path:
|
|
1532
|
+
sys.path.insert(0, str(_PROJECT_ROOT))
|
|
1533
|
+
from board_os import mcp_tools as _board_mcp # type: ignore
|
|
1534
|
+
|
|
1535
|
+
_BOARD_OS_AVAILABLE = True
|
|
1536
|
+
except ImportError as _exc:
|
|
1537
|
+
logger.warning("board_os MCP tools unavailable: %s", _exc)
|
|
1538
|
+
_BOARD_OS_AVAILABLE = False
|
|
1539
|
+
|
|
1540
|
+
|
|
1541
|
+
if _BOARD_OS_AVAILABLE:
|
|
1542
|
+
|
|
1543
|
+
@mcp.tool(
|
|
1544
|
+
name="cos_task_create",
|
|
1545
|
+
annotations={
|
|
1546
|
+
"title": "Create New Scrumban Task",
|
|
1547
|
+
"readOnlyHint": False,
|
|
1548
|
+
"destructiveHint": False,
|
|
1549
|
+
"idempotentHint": False,
|
|
1550
|
+
"openWorldHint": False,
|
|
1551
|
+
},
|
|
1552
|
+
)
|
|
1553
|
+
def cos_task_create(
|
|
1554
|
+
title: str,
|
|
1555
|
+
swimlane: str,
|
|
1556
|
+
kind: str,
|
|
1557
|
+
priority: str = "P2",
|
|
1558
|
+
appetite: str = "1d",
|
|
1559
|
+
epic: str = "",
|
|
1560
|
+
labels: list[str] | None = None,
|
|
1561
|
+
outcome: str = "",
|
|
1562
|
+
acceptance: str = "",
|
|
1563
|
+
repro: str = "",
|
|
1564
|
+
read_first: list[str] | None = None,
|
|
1565
|
+
depends_on: list[str] | None = None,
|
|
1566
|
+
status: str = "icebox",
|
|
1567
|
+
ready: bool = False,
|
|
1568
|
+
agent_session: str = "",
|
|
1569
|
+
) -> str:
|
|
1570
|
+
"""Create a new Scrumban task file + sync to DB.
|
|
1571
|
+
|
|
1572
|
+
Prefer this over hand-writing YAML. Validates swimlane against
|
|
1573
|
+
scrumban-config.yaml and kind against the 8-value enum. Pass
|
|
1574
|
+
ready=True to mark the task pullable in one shot; for bug-kind
|
|
1575
|
+
tasks pass acceptance= (G/W/T lines) and repro= so the create
|
|
1576
|
+
satisfies its own DoR in one call.
|
|
1577
|
+
"""
|
|
1578
|
+
resolved_session = agent_session or _detect_agent_session_default() or None
|
|
1579
|
+
return _board_mcp.cos_task_create(
|
|
1580
|
+
get_pooled_conn(),
|
|
1581
|
+
title=title,
|
|
1582
|
+
swimlane=swimlane,
|
|
1583
|
+
kind=kind,
|
|
1584
|
+
priority=priority,
|
|
1585
|
+
appetite=appetite,
|
|
1586
|
+
epic=epic or None,
|
|
1587
|
+
labels=labels or [],
|
|
1588
|
+
outcome=outcome or None,
|
|
1589
|
+
acceptance=acceptance or None,
|
|
1590
|
+
repro=repro or None,
|
|
1591
|
+
read_first=read_first or [],
|
|
1592
|
+
depends_on=depends_on or [],
|
|
1593
|
+
status=status,
|
|
1594
|
+
ready=ready,
|
|
1595
|
+
agent_session=resolved_session,
|
|
1596
|
+
)
|
|
1597
|
+
|
|
1598
|
+
@mcp.tool(
|
|
1599
|
+
name="cos_task_board",
|
|
1600
|
+
annotations={
|
|
1601
|
+
"title": "Scrumban Board State",
|
|
1602
|
+
"readOnlyHint": True,
|
|
1603
|
+
"destructiveHint": False,
|
|
1604
|
+
"idempotentHint": True,
|
|
1605
|
+
"openWorldHint": False,
|
|
1606
|
+
},
|
|
1607
|
+
)
|
|
1608
|
+
def cos_task_board(
|
|
1609
|
+
swimlane: str = "",
|
|
1610
|
+
kind: str = "",
|
|
1611
|
+
epic: str = "",
|
|
1612
|
+
status_filter: list[str] | None = None,
|
|
1613
|
+
include_archive: bool = False,
|
|
1614
|
+
limit: int = 50,
|
|
1615
|
+
page_size: int = 50,
|
|
1616
|
+
cursor: str = "",
|
|
1617
|
+
) -> str:
|
|
1618
|
+
"""Return the board state grouped by (swimlane, status) with WIP info. Complete/archive columns are keyset-paginated (pass cursor + status_filter to load more)."""
|
|
1619
|
+
return _board_mcp.cos_task_board(
|
|
1620
|
+
get_pooled_conn(),
|
|
1621
|
+
swimlane=swimlane or None,
|
|
1622
|
+
kind=kind or None,
|
|
1623
|
+
epic=epic or None,
|
|
1624
|
+
status_filter=status_filter,
|
|
1625
|
+
include_archive=include_archive,
|
|
1626
|
+
limit=limit,
|
|
1627
|
+
page_size=page_size,
|
|
1628
|
+
cursor=cursor or None,
|
|
1629
|
+
)
|
|
1630
|
+
|
|
1631
|
+
@mcp.tool(
|
|
1632
|
+
name="cos_task_show",
|
|
1633
|
+
annotations={
|
|
1634
|
+
"title": "Show Single Task (frontmatter + body)",
|
|
1635
|
+
"readOnlyHint": True,
|
|
1636
|
+
"destructiveHint": False,
|
|
1637
|
+
"idempotentHint": True,
|
|
1638
|
+
"openWorldHint": False,
|
|
1639
|
+
},
|
|
1640
|
+
)
|
|
1641
|
+
def cos_task_show(task_id: str, include_body: bool = True) -> str:
|
|
1642
|
+
"""Show a single task's frontmatter fields and full markdown body — in-session alternative to raw ls/grep/Read on docs/tasks."""
|
|
1643
|
+
return _board_mcp.cos_task_show(
|
|
1644
|
+
get_pooled_conn(),
|
|
1645
|
+
task_id=task_id,
|
|
1646
|
+
include_body=include_body,
|
|
1647
|
+
)
|
|
1648
|
+
|
|
1649
|
+
@mcp.tool(
|
|
1650
|
+
name="cos_task_history",
|
|
1651
|
+
annotations={
|
|
1652
|
+
"title": "Task History (create + transitions + edits + commits)",
|
|
1653
|
+
"readOnlyHint": True,
|
|
1654
|
+
"destructiveHint": False,
|
|
1655
|
+
"idempotentHint": True,
|
|
1656
|
+
"openWorldHint": False,
|
|
1657
|
+
},
|
|
1658
|
+
)
|
|
1659
|
+
def cos_task_history(task_id: str, include_commits: bool = True, limit: int = 200) -> str:
|
|
1660
|
+
"""Full actor-attributed task history — creation, status transitions, field edits, and git commits."""
|
|
1661
|
+
return _board_mcp.cos_task_history(
|
|
1662
|
+
get_pooled_conn(),
|
|
1663
|
+
task_id=task_id,
|
|
1664
|
+
include_commits=include_commits,
|
|
1665
|
+
limit=limit,
|
|
1666
|
+
)
|
|
1667
|
+
|
|
1668
|
+
@mcp.tool(
|
|
1669
|
+
name="cos_task_edit",
|
|
1670
|
+
annotations={
|
|
1671
|
+
"title": "Edit Task Fields / Body (actor-attributed)",
|
|
1672
|
+
"readOnlyHint": False,
|
|
1673
|
+
"destructiveHint": False,
|
|
1674
|
+
"idempotentHint": False,
|
|
1675
|
+
"openWorldHint": False,
|
|
1676
|
+
},
|
|
1677
|
+
)
|
|
1678
|
+
def cos_task_edit(
|
|
1679
|
+
task_id: str,
|
|
1680
|
+
title: str = "",
|
|
1681
|
+
priority: str = "",
|
|
1682
|
+
swimlane: str = "",
|
|
1683
|
+
appetite: str = "",
|
|
1684
|
+
epic: str = "",
|
|
1685
|
+
labels_csv: str = "",
|
|
1686
|
+
body: str = "",
|
|
1687
|
+
actor_type: str = "agent",
|
|
1688
|
+
actor_id: str = "",
|
|
1689
|
+
source: str = "mcp",
|
|
1690
|
+
) -> str:
|
|
1691
|
+
"""Edit a task's frontmatter fields and/or body; each change is recorded to the actor-attributed edit history."""
|
|
1692
|
+
return _board_mcp.cos_task_edit(
|
|
1693
|
+
get_pooled_conn(),
|
|
1694
|
+
task_id=task_id,
|
|
1695
|
+
title=title or None,
|
|
1696
|
+
priority=priority or None,
|
|
1697
|
+
swimlane=swimlane or None,
|
|
1698
|
+
appetite=appetite or None,
|
|
1699
|
+
epic=epic or None,
|
|
1700
|
+
labels=[s.strip() for s in labels_csv.split(",") if s.strip()] if labels_csv else None,
|
|
1701
|
+
body=body or None,
|
|
1702
|
+
actor_type=actor_type,
|
|
1703
|
+
actor_id=actor_id or None,
|
|
1704
|
+
source=source,
|
|
1705
|
+
)
|
|
1706
|
+
|
|
1707
|
+
@mcp.tool(
|
|
1708
|
+
name="cos_task_link",
|
|
1709
|
+
annotations={
|
|
1710
|
+
"title": "Link a Task to a Forge Issue/PR (external_ref)",
|
|
1711
|
+
"readOnlyHint": False,
|
|
1712
|
+
"destructiveHint": False,
|
|
1713
|
+
"idempotentHint": True,
|
|
1714
|
+
"openWorldHint": False,
|
|
1715
|
+
},
|
|
1716
|
+
)
|
|
1717
|
+
def cos_task_link(task_id: str, ref: str) -> str:
|
|
1718
|
+
"""Set a task's optional external_ref (e.g. github#42) — forge auto-detected; metadata only, never the id."""
|
|
1719
|
+
return _board_mcp.cos_task_link(get_pooled_conn(), task_id=task_id, ref=ref)
|
|
1720
|
+
|
|
1721
|
+
@mcp.tool(
|
|
1722
|
+
name="cos_presence_query",
|
|
1723
|
+
annotations={
|
|
1724
|
+
"title": "Live Agent Presence (sessions + states)",
|
|
1725
|
+
"readOnlyHint": True,
|
|
1726
|
+
"destructiveHint": False,
|
|
1727
|
+
"idempotentHint": True,
|
|
1728
|
+
"openWorldHint": False,
|
|
1729
|
+
},
|
|
1730
|
+
)
|
|
1731
|
+
def cos_presence_query(agent: str = "") -> str:
|
|
1732
|
+
"""Return per-agent presence state and live-session inventory.
|
|
1733
|
+
|
|
1734
|
+
Reads `.coding-os/<agent>/sessions/*.json` (the same files
|
|
1735
|
+
agent-presence.sh writes) and applies the SSOT rules in
|
|
1736
|
+
`board_os.presence`. When `agent` is empty, every adapter
|
|
1737
|
+
registered in adapters/<id>/adapter.yaml is reported.
|
|
1738
|
+
|
|
1739
|
+
Used by `cos daily`, CI gates, and the live-agents board UI to
|
|
1740
|
+
verify zombie sessions are gone after deploy.
|
|
1741
|
+
"""
|
|
1742
|
+
try:
|
|
1743
|
+
from board_os.hub_adapter_manifest import list_agent_manifest_rows
|
|
1744
|
+
from board_os.presence import (
|
|
1745
|
+
agent_state as _agent_state_q,
|
|
1746
|
+
session_inventory as _session_inventory_q,
|
|
1747
|
+
)
|
|
1748
|
+
except ImportError as exc:
|
|
1749
|
+
return fail(
|
|
1750
|
+
"unavailable",
|
|
1751
|
+
f"board_os presence module not importable: {exc}",
|
|
1752
|
+
retryable=False,
|
|
1753
|
+
)
|
|
1754
|
+
|
|
1755
|
+
# Resolve the project root the same way the web routes do so
|
|
1756
|
+
# multi-project servers inspect the right .coding-os/ tree.
|
|
1757
|
+
try:
|
|
1758
|
+
from web._project_context import current_project_root # type: ignore
|
|
1759
|
+
|
|
1760
|
+
root = current_project_root()
|
|
1761
|
+
except Exception as exc:
|
|
1762
|
+
return fail(
|
|
1763
|
+
"unavailable",
|
|
1764
|
+
f"cannot resolve project root: {exc}",
|
|
1765
|
+
retryable=False,
|
|
1766
|
+
)
|
|
1767
|
+
|
|
1768
|
+
agents = (
|
|
1769
|
+
[agent.strip()]
|
|
1770
|
+
if agent.strip()
|
|
1771
|
+
else [str(r.get("id") or "") for r in list_agent_manifest_rows() if r.get("id")]
|
|
1772
|
+
)
|
|
1773
|
+
states: dict[str, str] = {}
|
|
1774
|
+
sessions: list[dict] = []
|
|
1775
|
+
for aid in agents:
|
|
1776
|
+
if not aid:
|
|
1777
|
+
continue
|
|
1778
|
+
d = root / ".coding-os" / aid / "sessions"
|
|
1779
|
+
states[aid] = _agent_state_q(d)
|
|
1780
|
+
sessions.extend(_session_inventory_q(aid, d))
|
|
1781
|
+
return ok(
|
|
1782
|
+
{
|
|
1783
|
+
"agent_states": states,
|
|
1784
|
+
"session_states": sessions,
|
|
1785
|
+
"session_counts": {
|
|
1786
|
+
aid: sum(1 for s in sessions if s["agent"] == aid) for aid in agents
|
|
1787
|
+
},
|
|
1788
|
+
"scope": "per_project",
|
|
1789
|
+
"root": str(root),
|
|
1790
|
+
}
|
|
1791
|
+
)
|
|
1792
|
+
|
|
1793
|
+
@mcp.tool(
|
|
1794
|
+
name="cos_task_move",
|
|
1795
|
+
annotations={
|
|
1796
|
+
"title": "Move Task to New Status",
|
|
1797
|
+
"readOnlyHint": False,
|
|
1798
|
+
"destructiveHint": False,
|
|
1799
|
+
"idempotentHint": False,
|
|
1800
|
+
"openWorldHint": False,
|
|
1801
|
+
},
|
|
1802
|
+
)
|
|
1803
|
+
def cos_task_move(
|
|
1804
|
+
task_id: str,
|
|
1805
|
+
to: str,
|
|
1806
|
+
reason: str = "",
|
|
1807
|
+
bypass_wip: bool = False,
|
|
1808
|
+
agent_session: str = "",
|
|
1809
|
+
) -> str:
|
|
1810
|
+
"""Transition a task through the Scrumban state machine."""
|
|
1811
|
+
resolved_session = agent_session or _detect_agent_session_default() or None
|
|
1812
|
+
return _board_mcp.cos_task_move(
|
|
1813
|
+
get_pooled_conn(),
|
|
1814
|
+
task_id=task_id,
|
|
1815
|
+
to=to,
|
|
1816
|
+
reason=reason or "mcp:cos_task_move (no reason given)",
|
|
1817
|
+
bypass_wip=bypass_wip,
|
|
1818
|
+
agent_session=resolved_session,
|
|
1819
|
+
)
|
|
1820
|
+
|
|
1821
|
+
@mcp.tool(
|
|
1822
|
+
name="cos_task_reposition",
|
|
1823
|
+
annotations={
|
|
1824
|
+
"title": "Reposition Task (status and/or swimlane)",
|
|
1825
|
+
"readOnlyHint": False,
|
|
1826
|
+
"destructiveHint": False,
|
|
1827
|
+
"idempotentHint": False,
|
|
1828
|
+
"openWorldHint": False,
|
|
1829
|
+
},
|
|
1830
|
+
)
|
|
1831
|
+
def cos_task_reposition(
|
|
1832
|
+
task_id: str,
|
|
1833
|
+
swimlane: str = "",
|
|
1834
|
+
to: str = "",
|
|
1835
|
+
reason: str = "",
|
|
1836
|
+
bypass_wip: bool = False,
|
|
1837
|
+
agent_session: str = "",
|
|
1838
|
+
) -> str:
|
|
1839
|
+
"""Update Scrumban status and/or swimlane (MD frontmatter + sync)."""
|
|
1840
|
+
resolved_session = agent_session or _detect_agent_session_default() or None
|
|
1841
|
+
return _board_mcp.cos_task_reposition(
|
|
1842
|
+
get_pooled_conn(),
|
|
1843
|
+
task_id=task_id,
|
|
1844
|
+
swimlane=swimlane or None,
|
|
1845
|
+
to=to or None,
|
|
1846
|
+
reason=reason or None,
|
|
1847
|
+
bypass_wip=bypass_wip,
|
|
1848
|
+
agent_session=resolved_session,
|
|
1849
|
+
)
|
|
1850
|
+
|
|
1851
|
+
@mcp.tool(
|
|
1852
|
+
name="cos_task_ready",
|
|
1853
|
+
annotations={
|
|
1854
|
+
"title": "Mark Task Ready (toggle pull-gate label)",
|
|
1855
|
+
"readOnlyHint": False,
|
|
1856
|
+
"destructiveHint": False,
|
|
1857
|
+
"idempotentHint": True,
|
|
1858
|
+
"openWorldHint": False,
|
|
1859
|
+
},
|
|
1860
|
+
)
|
|
1861
|
+
def cos_task_ready(
|
|
1862
|
+
task_id: str,
|
|
1863
|
+
ready: bool = True,
|
|
1864
|
+
agent_session: str = "",
|
|
1865
|
+
) -> str:
|
|
1866
|
+
"""Add or remove the 'ready' label that gates icebox→in_progress."""
|
|
1867
|
+
resolved_session = agent_session or _detect_agent_session_default() or None
|
|
1868
|
+
return _board_mcp.cos_task_ready(
|
|
1869
|
+
get_pooled_conn(),
|
|
1870
|
+
task_id=task_id,
|
|
1871
|
+
ready=ready,
|
|
1872
|
+
agent_session=resolved_session,
|
|
1873
|
+
)
|
|
1874
|
+
|
|
1875
|
+
@mcp.tool(
|
|
1876
|
+
name="cos_task_reclaim",
|
|
1877
|
+
annotations={
|
|
1878
|
+
"title": "Reclaim Zombie in_progress Tasks",
|
|
1879
|
+
"readOnlyHint": False,
|
|
1880
|
+
"destructiveHint": False,
|
|
1881
|
+
"idempotentHint": True,
|
|
1882
|
+
"openWorldHint": False,
|
|
1883
|
+
},
|
|
1884
|
+
)
|
|
1885
|
+
def cos_task_reclaim(
|
|
1886
|
+
idle_hours: int = 0,
|
|
1887
|
+
dry_run: bool = False,
|
|
1888
|
+
agent_session: str = "",
|
|
1889
|
+
) -> str:
|
|
1890
|
+
"""Reclaim zombie in_progress tasks (idle + owner session inactive) to icebox+ready."""
|
|
1891
|
+
resolved_session = agent_session or _detect_agent_session_default() or None
|
|
1892
|
+
return _board_mcp.cos_task_reclaim(
|
|
1893
|
+
get_pooled_conn(),
|
|
1894
|
+
idle_hours=idle_hours or None,
|
|
1895
|
+
dry_run=dry_run,
|
|
1896
|
+
agent_session=resolved_session,
|
|
1897
|
+
)
|
|
1898
|
+
|
|
1899
|
+
@mcp.tool(
|
|
1900
|
+
name="cos_task_reconcile",
|
|
1901
|
+
annotations={
|
|
1902
|
+
"title": "Reconcile Stranded Tasks (review-first)",
|
|
1903
|
+
"readOnlyHint": True,
|
|
1904
|
+
"destructiveHint": False,
|
|
1905
|
+
"idempotentHint": True,
|
|
1906
|
+
"openWorldHint": False,
|
|
1907
|
+
},
|
|
1908
|
+
)
|
|
1909
|
+
def cos_task_reconcile(include_active: bool = False) -> str:
|
|
1910
|
+
"""Triage stranded in_progress/testing tasks with completion evidence + a review recommendation (read-only)."""
|
|
1911
|
+
return _board_mcp.cos_task_reconcile(get_pooled_conn(), include_active=include_active)
|
|
1912
|
+
|
|
1913
|
+
@mcp.tool(
|
|
1914
|
+
name="cos_task_pick",
|
|
1915
|
+
annotations={
|
|
1916
|
+
"title": "Pick Next Task to Work On",
|
|
1917
|
+
"readOnlyHint": True,
|
|
1918
|
+
"destructiveHint": False,
|
|
1919
|
+
"idempotentHint": True,
|
|
1920
|
+
"openWorldHint": False,
|
|
1921
|
+
},
|
|
1922
|
+
)
|
|
1923
|
+
def cos_task_pick(
|
|
1924
|
+
swimlane: str = "",
|
|
1925
|
+
priority_min: str = "P2",
|
|
1926
|
+
max_candidates: int = 5,
|
|
1927
|
+
) -> str:
|
|
1928
|
+
"""Return top candidate tasks to start next, ranked by priority."""
|
|
1929
|
+
return _board_mcp.cos_task_pick(
|
|
1930
|
+
get_pooled_conn(),
|
|
1931
|
+
swimlane=swimlane or None,
|
|
1932
|
+
priority_min=priority_min,
|
|
1933
|
+
max_candidates=max_candidates,
|
|
1934
|
+
)
|
|
1935
|
+
|
|
1936
|
+
@mcp.tool(
|
|
1937
|
+
name="cos_task_claim_next",
|
|
1938
|
+
annotations={
|
|
1939
|
+
"title": "Atomically Claim Next Runnable Task",
|
|
1940
|
+
"readOnlyHint": False,
|
|
1941
|
+
"destructiveHint": False,
|
|
1942
|
+
"idempotentHint": False,
|
|
1943
|
+
"openWorldHint": False,
|
|
1944
|
+
},
|
|
1945
|
+
)
|
|
1946
|
+
def cos_task_claim_next(
|
|
1947
|
+
swimlane: str = "",
|
|
1948
|
+
priority_min: str = "P2",
|
|
1949
|
+
agent_session: str = "",
|
|
1950
|
+
) -> str:
|
|
1951
|
+
"""Atomically select+claim the top runnable task for this session (or claimed=null)."""
|
|
1952
|
+
return _board_mcp.cos_task_claim_next(
|
|
1953
|
+
get_pooled_conn(),
|
|
1954
|
+
swimlane=swimlane or None,
|
|
1955
|
+
priority_min=priority_min,
|
|
1956
|
+
agent_session=agent_session or None,
|
|
1957
|
+
)
|
|
1958
|
+
|
|
1959
|
+
@mcp.tool(
|
|
1960
|
+
name="cos_task_daily",
|
|
1961
|
+
annotations={
|
|
1962
|
+
"title": "Daily Standup Summary",
|
|
1963
|
+
"readOnlyHint": True,
|
|
1964
|
+
"destructiveHint": False,
|
|
1965
|
+
"idempotentHint": True,
|
|
1966
|
+
"openWorldHint": False,
|
|
1967
|
+
},
|
|
1968
|
+
)
|
|
1969
|
+
def cos_task_daily(since: str = "24h", agent_session: str = "") -> str:
|
|
1970
|
+
"""Produce the daily standup summary."""
|
|
1971
|
+
return _board_mcp.cos_task_daily(
|
|
1972
|
+
get_pooled_conn(),
|
|
1973
|
+
since=since,
|
|
1974
|
+
agent_session=agent_session or None,
|
|
1975
|
+
)
|
|
1976
|
+
|
|
1977
|
+
@mcp.tool(
|
|
1978
|
+
name="cos_task_retro",
|
|
1979
|
+
annotations={
|
|
1980
|
+
"title": "Weekly Retrospective",
|
|
1981
|
+
"readOnlyHint": True,
|
|
1982
|
+
"destructiveHint": False,
|
|
1983
|
+
"idempotentHint": True,
|
|
1984
|
+
"openWorldHint": False,
|
|
1985
|
+
},
|
|
1986
|
+
)
|
|
1987
|
+
def cos_task_retro(since: str = "7d") -> str:
|
|
1988
|
+
"""Weekly retro metrics (cycle time, throughput, emergency count)."""
|
|
1989
|
+
return _board_mcp.cos_task_retro(get_pooled_conn(), since=since)
|
|
1990
|
+
|
|
1991
|
+
@mcp.tool(
|
|
1992
|
+
name="cos_task_wip_check",
|
|
1993
|
+
annotations={
|
|
1994
|
+
"title": "WIP Cap Health Check",
|
|
1995
|
+
"readOnlyHint": True,
|
|
1996
|
+
"destructiveHint": False,
|
|
1997
|
+
"idempotentHint": True,
|
|
1998
|
+
"openWorldHint": False,
|
|
1999
|
+
},
|
|
2000
|
+
)
|
|
2001
|
+
def cos_task_wip_check() -> str:
|
|
2002
|
+
"""Lightweight check of current WIP counts vs. configured caps."""
|
|
2003
|
+
return _board_mcp.cos_task_wip_check(get_pooled_conn())
|
|
2004
|
+
|
|
2005
|
+
@mcp.tool(
|
|
2006
|
+
name="cos_work_log_append",
|
|
2007
|
+
annotations={
|
|
2008
|
+
"title": "Append Line to Task Work Log",
|
|
2009
|
+
"readOnlyHint": False,
|
|
2010
|
+
"destructiveHint": False,
|
|
2011
|
+
"idempotentHint": False,
|
|
2012
|
+
"openWorldHint": False,
|
|
2013
|
+
},
|
|
2014
|
+
)
|
|
2015
|
+
def cos_work_log_append(
|
|
2016
|
+
task_id: str,
|
|
2017
|
+
summary: str,
|
|
2018
|
+
agent_session: str = "",
|
|
2019
|
+
source: str = "manual",
|
|
2020
|
+
) -> str:
|
|
2021
|
+
"""Append one Work Log line to a task. Critical for Codex sessions."""
|
|
2022
|
+
resolved_session = agent_session or _detect_agent_session_default() or None
|
|
2023
|
+
return _board_mcp.cos_work_log_append(
|
|
2024
|
+
get_pooled_conn(),
|
|
2025
|
+
task_id=task_id,
|
|
2026
|
+
summary=summary,
|
|
2027
|
+
agent_session=resolved_session,
|
|
2028
|
+
source=source,
|
|
2029
|
+
)
|
|
2030
|
+
|
|
2031
|
+
|
|
2032
|
+
# ---------------------------------------------------------------------------
|
|
2033
|
+
# Retrieval feedback
|
|
2034
|
+
# ---------------------------------------------------------------------------
|
|
2035
|
+
@mcp.tool(
|
|
2036
|
+
name="cos_retrieval_cite",
|
|
2037
|
+
annotations={
|
|
2038
|
+
"title": "Cite Retrievals the Agent Used",
|
|
2039
|
+
"readOnlyHint": False,
|
|
2040
|
+
"destructiveHint": False,
|
|
2041
|
+
"idempotentHint": True,
|
|
2042
|
+
"openWorldHint": False,
|
|
2043
|
+
},
|
|
2044
|
+
)
|
|
2045
|
+
@safe_tool
|
|
2046
|
+
def cos_retrieval_cite(retrieval_ids: str) -> str:
|
|
2047
|
+
"""Mark retrieval rows as actively cited by the agent.
|
|
2048
|
+
|
|
2049
|
+
Call this after using one or more chunks/patterns/tasks in a meaningful
|
|
2050
|
+
way (read them carefully, applied them). Cited retrievals get ~4× the
|
|
2051
|
+
weight when priority-learning runs, so the signal is only useful if it
|
|
2052
|
+
reflects actual use — do NOT cite passive retrievals.
|
|
2053
|
+
|
|
2054
|
+
Args:
|
|
2055
|
+
retrieval_ids: Comma-separated list of retrieval ids (int), returned
|
|
2056
|
+
as `retrieval_ids` in prior cos_search / cos_doc_search /
|
|
2057
|
+
cos_task_search responses. e.g. "12,17,24".
|
|
2058
|
+
|
|
2059
|
+
Returns:
|
|
2060
|
+
JSON with `{updated, unknown}` — updated count + list of ids that
|
|
2061
|
+
did not exist.
|
|
2062
|
+
"""
|
|
2063
|
+
try:
|
|
2064
|
+
ids = [int(x) for x in retrieval_ids.split(",") if x.strip()]
|
|
2065
|
+
except ValueError:
|
|
2066
|
+
raise ValueError("retrieval_ids must be comma-separated integers")
|
|
2067
|
+
result = cite_retrievals(_db_conn, ids)
|
|
2068
|
+
return ok(result, meta={"layer": "learning"})
|
|
2069
|
+
|
|
2070
|
+
|
|
2071
|
+
@mcp.tool(
|
|
2072
|
+
name="cos_retrieval_learn",
|
|
2073
|
+
annotations={
|
|
2074
|
+
"title": "Priority Learning from Retrieval Outcomes",
|
|
2075
|
+
"readOnlyHint": False,
|
|
2076
|
+
"destructiveHint": False,
|
|
2077
|
+
"idempotentHint": False,
|
|
2078
|
+
"openWorldHint": False,
|
|
2079
|
+
},
|
|
2080
|
+
)
|
|
2081
|
+
@safe_tool
|
|
2082
|
+
def cos_retrieval_learn(lookback_days: int = 7, dry_run: bool = False) -> str:
|
|
2083
|
+
"""Adjust document_chunks.priority based on recent retrieval outcomes.
|
|
2084
|
+
|
|
2085
|
+
Walks retrievals with a known outcome in the lookback window and:
|
|
2086
|
+
- chunk cited in a success task → priority += 0.02
|
|
2087
|
+
- chunk cited in a rework/blocked task → priority −= 0.01
|
|
2088
|
+
- passive retrievals ±0.005 (weaker signal)
|
|
2089
|
+
|
|
2090
|
+
Clamped to [0.1, 0.9]. Intended to run nightly via cron or after a
|
|
2091
|
+
batch of task-done events.
|
|
2092
|
+
|
|
2093
|
+
Args:
|
|
2094
|
+
lookback_days: How many days of retrievals to consider (default 7).
|
|
2095
|
+
dry_run: When True, compute changes without writing.
|
|
2096
|
+
|
|
2097
|
+
Returns:
|
|
2098
|
+
`{adjusted, gained, lost, changes[], status}` envelope.
|
|
2099
|
+
"""
|
|
2100
|
+
result = learn_from_retrievals(
|
|
2101
|
+
_db_conn, lookback_days=int(lookback_days), dry_run=bool(dry_run)
|
|
2102
|
+
)
|
|
2103
|
+
return ok(result, meta={"layer": "learning"})
|
|
2104
|
+
|
|
2105
|
+
|
|
2106
|
+
# ---------------------------------------------------------------------------
|
|
2107
|
+
# Agent digest
|
|
2108
|
+
# ---------------------------------------------------------------------------
|
|
2109
|
+
@mcp.tool(
|
|
2110
|
+
name="cos_digest_regenerate",
|
|
2111
|
+
annotations={
|
|
2112
|
+
"title": "Regenerate Agent Digest",
|
|
2113
|
+
"readOnlyHint": False,
|
|
2114
|
+
"destructiveHint": False,
|
|
2115
|
+
"idempotentHint": True,
|
|
2116
|
+
"openWorldHint": False,
|
|
2117
|
+
},
|
|
2118
|
+
)
|
|
2119
|
+
@safe_tool
|
|
2120
|
+
def cos_digest_regenerate(project_root: str = "") -> str:
|
|
2121
|
+
"""Refresh `.coding-os/digest.md` from current memory state.
|
|
2122
|
+
|
|
2123
|
+
The digest is a ≤ 2.4 KB rolling snapshot of the agent's identity:
|
|
2124
|
+
active beliefs, fading patterns, recent breakthroughs, preferences.
|
|
2125
|
+
Session-startup reads this file to give the agent a coherent
|
|
2126
|
+
memory anchor before any retrieval fires.
|
|
2127
|
+
|
|
2128
|
+
Args:
|
|
2129
|
+
project_root: Override project root. Empty (default) uses cwd.
|
|
2130
|
+
|
|
2131
|
+
Returns:
|
|
2132
|
+
`{path, size_chars, truncated, status}` envelope.
|
|
2133
|
+
"""
|
|
2134
|
+
import os
|
|
2135
|
+
from pathlib import Path
|
|
2136
|
+
|
|
2137
|
+
from digest import regenerate
|
|
2138
|
+
|
|
2139
|
+
root = Path(project_root) if project_root else Path(os.environ.get("COS_PROJECT_ROOT", "."))
|
|
2140
|
+
result = regenerate(_db_conn, project_root=root)
|
|
2141
|
+
return ok(result, meta={"layer": "learning"})
|
|
2142
|
+
|
|
2143
|
+
|
|
2144
|
+
# ---------------------------------------------------------------------------
|
|
2145
|
+
# Retrieval quality / enrichment gate
|
|
2146
|
+
# ---------------------------------------------------------------------------
|
|
2147
|
+
@mcp.tool(
|
|
2148
|
+
name="cos_retrieval_quality",
|
|
2149
|
+
annotations={
|
|
2150
|
+
"title": "Retrieval Precision Summary",
|
|
2151
|
+
"readOnlyHint": True,
|
|
2152
|
+
"destructiveHint": False,
|
|
2153
|
+
"idempotentHint": True,
|
|
2154
|
+
"openWorldHint": False,
|
|
2155
|
+
},
|
|
2156
|
+
)
|
|
2157
|
+
@safe_tool
|
|
2158
|
+
def cos_retrieval_quality(lookback_days: int = 14, layer: str = "") -> str:
|
|
2159
|
+
"""Report mean retrieval precision over the lookback window.
|
|
2160
|
+
|
|
2161
|
+
Precision is derived from (was_cited, outcome) pairs on the
|
|
2162
|
+
retrievals table, so it's honest: a retrieval that was cited and
|
|
2163
|
+
led to success counts as 1.0; a cited retrieval that led to rework
|
|
2164
|
+
counts as 0.0. Used to decide whether contextual enrichment is worth
|
|
2165
|
+
the LLM cost.
|
|
2166
|
+
|
|
2167
|
+
Args:
|
|
2168
|
+
lookback_days: Window in days (default 14).
|
|
2169
|
+
layer: Optional layer filter ("memory"|"docs"|"tasks").
|
|
2170
|
+
|
|
2171
|
+
Returns:
|
|
2172
|
+
`{mean_precision, samples, below_gate, gate, layer, status}`.
|
|
2173
|
+
"""
|
|
2174
|
+
from retrieval_quality import backfill_quality_from_outcomes, precision_summary
|
|
2175
|
+
|
|
2176
|
+
# Idempotent: ensure quality rows are up to date before summarising
|
|
2177
|
+
backfill_quality_from_outcomes(_db_conn, lookback_days=int(lookback_days))
|
|
2178
|
+
result = precision_summary(
|
|
2179
|
+
_db_conn,
|
|
2180
|
+
lookback_days=int(lookback_days),
|
|
2181
|
+
layer=layer or None,
|
|
2182
|
+
)
|
|
2183
|
+
return ok(result, meta={"layer": "metrics"})
|
|
2184
|
+
|
|
2185
|
+
|
|
2186
|
+
@mcp.tool(
|
|
2187
|
+
name="cos_retrieval_enrichment_check",
|
|
2188
|
+
annotations={
|
|
2189
|
+
"title": "Contextual Enrichment Recommendation",
|
|
2190
|
+
"readOnlyHint": True,
|
|
2191
|
+
"destructiveHint": False,
|
|
2192
|
+
"idempotentHint": True,
|
|
2193
|
+
"openWorldHint": False,
|
|
2194
|
+
},
|
|
2195
|
+
)
|
|
2196
|
+
@safe_tool
|
|
2197
|
+
def cos_retrieval_enrichment_check(lookback_days: int = 14) -> str:
|
|
2198
|
+
"""Recommend whether to enable contextual retrieval enrichment.
|
|
2199
|
+
|
|
2200
|
+
The underlying LLM enrichment path is intentionally a stub — this tool
|
|
2201
|
+
exists so the *decision* is metric-driven and auditable before anyone
|
|
2202
|
+
pays the Haiku bill.
|
|
2203
|
+
|
|
2204
|
+
Args:
|
|
2205
|
+
lookback_days: Window of retrieval quality data (default 14).
|
|
2206
|
+
|
|
2207
|
+
Returns:
|
|
2208
|
+
`{recommend: bool, reason, cost_warning?, summary}`.
|
|
2209
|
+
"""
|
|
2210
|
+
from retrieval_quality import backfill_quality_from_outcomes, should_enable_enrichment
|
|
2211
|
+
|
|
2212
|
+
backfill_quality_from_outcomes(_db_conn, lookback_days=int(lookback_days))
|
|
2213
|
+
result = should_enable_enrichment(_db_conn, lookback_days=int(lookback_days))
|
|
2214
|
+
return ok(result, meta={"layer": "metrics"})
|
|
2215
|
+
|
|
2216
|
+
|
|
2217
|
+
# ---------------------------------------------------------------------------
|
|
2218
|
+
# 9 formula-agent supervisor tools.
|
|
2219
|
+
# 3 role-based routing tools (cos_analyze_task, cos_compose_chain,
|
|
2220
|
+
# cos_role_info).
|
|
2221
|
+
# 2 dispatch tools.
|
|
2222
|
+
# ---------------------------------------------------------------------------
|
|
2223
|
+
try:
|
|
2224
|
+
from database import DEFAULT_DB_PATH as _DEFAULT_DB_PATH
|
|
2225
|
+
from tools.cognition import register_all as _register_cognition_tools
|
|
2226
|
+
|
|
2227
|
+
_register_cognition_tools(mcp, str(_DEFAULT_DB_PATH))
|
|
2228
|
+
logger.info("Cognition tools registered")
|
|
2229
|
+
except Exception as _cog_exc: # pragma: no cover
|
|
2230
|
+
logger.warning("cognition tools unavailable: %s", _cog_exc)
|
|
2231
|
+
|
|
2232
|
+
|
|
2233
|
+
# ---------------------------------------------------------------------------
|
|
2234
|
+
# cos_graph_* MCP tools (knowledge-graph layer).
|
|
2235
|
+
#
|
|
2236
|
+
# The implementations live in `core/graph_os/tools/graph.py`; the wrappers
|
|
2237
|
+
# here expose them via FastMCP with MCP-friendly parameter types (comma-
|
|
2238
|
+
# separated strings instead of Sequence[str], etc.). Every wrapper stays
|
|
2239
|
+
# envelope-compliant because the underlying functions already route
|
|
2240
|
+
# through ok()/fail().
|
|
2241
|
+
# ---------------------------------------------------------------------------
|
|
2242
|
+
try:
|
|
2243
|
+
from graph_os.tools import (
|
|
2244
|
+
graph as _graph_tools,
|
|
2245
|
+
)
|
|
2246
|
+
|
|
2247
|
+
_GRAPH_TOOLS_AVAILABLE = True
|
|
2248
|
+
except ImportError as _graph_import_exc: # pragma: no cover — defensive
|
|
2249
|
+
logger.warning("graph_os tools unavailable: %s", _graph_import_exc)
|
|
2250
|
+
_graph_tools = None # type: ignore[assignment]
|
|
2251
|
+
_GRAPH_TOOLS_AVAILABLE = False
|
|
2252
|
+
|
|
2253
|
+
|
|
2254
|
+
def _csv(value: str) -> list[str] | None:
|
|
2255
|
+
"""Parse a comma-separated CLI-style string into a clean list or None."""
|
|
2256
|
+
if not value:
|
|
2257
|
+
return None
|
|
2258
|
+
parts = [p.strip() for p in value.split(",") if p.strip()]
|
|
2259
|
+
return parts or None
|
|
2260
|
+
|
|
2261
|
+
|
|
2262
|
+
def _graph_unavailable() -> str:
|
|
2263
|
+
"""Envelope the agent sees when graph_os tools can't be imported.
|
|
2264
|
+
|
|
2265
|
+
B20: MCP tool returns must be JSON-encoded strings. ``fail()`` from
|
|
2266
|
+
``tools._shared`` already returns ``json.dumps(...)`` so this
|
|
2267
|
+
function always returns a ``str``. The explicit ``json.dumps`` wrapper
|
|
2268
|
+
below makes the contract unambiguous should the import path change.
|
|
2269
|
+
"""
|
|
2270
|
+
import json as _json
|
|
2271
|
+
|
|
2272
|
+
return _json.dumps(
|
|
2273
|
+
{
|
|
2274
|
+
"ok": False,
|
|
2275
|
+
"error": {
|
|
2276
|
+
"category": "unavailable",
|
|
2277
|
+
"retryable": False,
|
|
2278
|
+
"message": "graph_os package not importable; install graph_os extra",
|
|
2279
|
+
},
|
|
2280
|
+
}
|
|
2281
|
+
)
|
|
2282
|
+
|
|
2283
|
+
|
|
2284
|
+
if _GRAPH_TOOLS_AVAILABLE:
|
|
2285
|
+
|
|
2286
|
+
@mcp.tool(
|
|
2287
|
+
name="cos_graph_query",
|
|
2288
|
+
annotations={
|
|
2289
|
+
"title": "Graph Symbol Lookup (known name/path/uid)",
|
|
2290
|
+
"readOnlyHint": True,
|
|
2291
|
+
"destructiveHint": False,
|
|
2292
|
+
"idempotentHint": True,
|
|
2293
|
+
"openWorldHint": False,
|
|
2294
|
+
},
|
|
2295
|
+
)
|
|
2296
|
+
@safe_tool
|
|
2297
|
+
def cos_graph_query_tool(
|
|
2298
|
+
q: str,
|
|
2299
|
+
kinds: str = "",
|
|
2300
|
+
limit: int = 10,
|
|
2301
|
+
max_hops: int = 2,
|
|
2302
|
+
confidence_min: float = 0.3,
|
|
2303
|
+
include_spine: bool = False,
|
|
2304
|
+
) -> str:
|
|
2305
|
+
"""Look up a symbol by a KNOWN short term, path, or uid (lexical + graph expansion). For a natural-language DESCRIPTION of code whose name you don't know, use cos_graph_search instead.
|
|
2306
|
+
|
|
2307
|
+
TIP: prefer SHORT terms ("sdk_dispatcher", "ClaudeSDKDispatcher.dispatch") or
|
|
2308
|
+
a literal path / uid. Long natural-language queries return weaker matches
|
|
2309
|
+
because the index is built from labels + docstrings, not free text.
|
|
2310
|
+
|
|
2311
|
+
UID scheme (also accepted as `q`):
|
|
2312
|
+
code:file:<path> · code:function:<path>::<name> · code:class:<path>::<name>
|
|
2313
|
+
code:method:<path>::<class>.<name> · code:module:<dotted>
|
|
2314
|
+
doc:file:<path> · doc:heading:<path>#<slug>:<level> · folder:<path>
|
|
2315
|
+
|
|
2316
|
+
When the query looks like a path or uid and the lexical pass
|
|
2317
|
+
returns nothing, the tool falls back to a direct uid lookup so
|
|
2318
|
+
the agent gets a single-item hit instead of empty results.
|
|
2319
|
+
|
|
2320
|
+
Args:
|
|
2321
|
+
q: Short term, path, or uid (non-empty). NL queries work but degrade.
|
|
2322
|
+
kinds: Comma-separated filter of node kinds (e.g. "function,class,method"). Empty = all.
|
|
2323
|
+
limit: Max results (default 10).
|
|
2324
|
+
max_hops: Walk expansion depth (default 2).
|
|
2325
|
+
confidence_min: Edge confidence floor (default 0.3).
|
|
2326
|
+
include_spine: S3 — attach the CONTAINS-ancestor chain to each result for breadcrumbs.
|
|
2327
|
+
|
|
2328
|
+
Returns:
|
|
2329
|
+
JSON envelope with `results` array. See docs/engineering/graph_os-queries.md.
|
|
2330
|
+
"""
|
|
2331
|
+
return _graph_tools.cos_graph_query(
|
|
2332
|
+
q,
|
|
2333
|
+
kinds=_csv(kinds),
|
|
2334
|
+
limit=int(limit),
|
|
2335
|
+
max_hops=int(max_hops),
|
|
2336
|
+
confidence_min=float(confidence_min),
|
|
2337
|
+
include_spine=bool(include_spine),
|
|
2338
|
+
)
|
|
2339
|
+
|
|
2340
|
+
@mcp.tool(
|
|
2341
|
+
name="cos_graph_context",
|
|
2342
|
+
annotations={
|
|
2343
|
+
"title": "Graph Neighbourhood",
|
|
2344
|
+
"readOnlyHint": True,
|
|
2345
|
+
"destructiveHint": False,
|
|
2346
|
+
"idempotentHint": True,
|
|
2347
|
+
"openWorldHint": False,
|
|
2348
|
+
},
|
|
2349
|
+
)
|
|
2350
|
+
@safe_tool
|
|
2351
|
+
def cos_graph_context_tool(
|
|
2352
|
+
uid_or_name: str,
|
|
2353
|
+
direction: str = "both",
|
|
2354
|
+
depth: int = 1,
|
|
2355
|
+
include_content: bool = False,
|
|
2356
|
+
include_evidence: bool = False,
|
|
2357
|
+
include_spine: bool = False,
|
|
2358
|
+
) -> str:
|
|
2359
|
+
"""Return callers + callees + siblings + referenced docs around a symbol.
|
|
2360
|
+
|
|
2361
|
+
Args:
|
|
2362
|
+
uid_or_name: Node uid or fuzzy label. Uid scheme:
|
|
2363
|
+
``code:file:<path>`` | ``code:function:<path>::<name>`` |
|
|
2364
|
+
``code:class:<path>::<name>`` | ``code:module:<dotted>`` |
|
|
2365
|
+
``doc:file:<path>`` | ``doc:heading:<path>#<slug>:<level>`` |
|
|
2366
|
+
``folder:<path>``. Raw repo paths (``core/foo.py``) are
|
|
2367
|
+
auto-resolved to ``code:file:`` / ``doc:file:`` / ``folder:``;
|
|
2368
|
+
if all variants miss, a fuzzy label match is tried. Run
|
|
2369
|
+
``cos_graph_query`` first to discover candidates.
|
|
2370
|
+
direction: "in" | "out" | "both".
|
|
2371
|
+
depth: BFS depth (default 1).
|
|
2372
|
+
include_content: When True, each returned node gains a ``content``
|
|
2373
|
+
field with source text read from ``file_path:start_line..end_line``
|
|
2374
|
+
(capped at 2000 chars, with ``truncated: bool``). Silently skipped
|
|
2375
|
+
when the file is missing or the node has no file_path. (B21)
|
|
2376
|
+
include_evidence: JOIN evidence rows (costs ~2× tokens).
|
|
2377
|
+
include_spine: S3 — pulls the CONTAINS-ancestor chain (file → folder → …)
|
|
2378
|
+
so the UI can render breadcrumbs.
|
|
2379
|
+
"""
|
|
2380
|
+
return _graph_tools.cos_graph_context(
|
|
2381
|
+
uid_or_name,
|
|
2382
|
+
direction=str(direction),
|
|
2383
|
+
depth=int(depth),
|
|
2384
|
+
include_content=bool(include_content),
|
|
2385
|
+
include_evidence=bool(include_evidence),
|
|
2386
|
+
include_spine=bool(include_spine),
|
|
2387
|
+
)
|
|
2388
|
+
|
|
2389
|
+
@mcp.tool(
|
|
2390
|
+
name="cos_graph_impact",
|
|
2391
|
+
annotations={
|
|
2392
|
+
"title": "Graph Blast-Radius",
|
|
2393
|
+
"readOnlyHint": True,
|
|
2394
|
+
"destructiveHint": False,
|
|
2395
|
+
"idempotentHint": True,
|
|
2396
|
+
"openWorldHint": False,
|
|
2397
|
+
},
|
|
2398
|
+
)
|
|
2399
|
+
@safe_tool
|
|
2400
|
+
def cos_graph_impact_tool(
|
|
2401
|
+
uid: str,
|
|
2402
|
+
direction: str = "downstream",
|
|
2403
|
+
depth: int = 3,
|
|
2404
|
+
confidence_min: float = 0.3,
|
|
2405
|
+
visit_limit: int = 500,
|
|
2406
|
+
) -> str:
|
|
2407
|
+
"""Group affected nodes by risk tier (will_break / should_review / context).
|
|
2408
|
+
|
|
2409
|
+
Args:
|
|
2410
|
+
uid: Fully-qualified node uid. Scheme: ``code:file:<path>`` |
|
|
2411
|
+
``code:function:<path>::<name>`` | ``code:class:<path>::<name>`` |
|
|
2412
|
+
``code:module:<dotted>`` | ``doc:file:<path>`` | ``folder:<path>``.
|
|
2413
|
+
Raw repo paths (``core/foo.py``) are auto-resolved to
|
|
2414
|
+
``code:file:`` / ``doc:file:`` / ``folder:``. If unsure, run
|
|
2415
|
+
``cos_graph_query`` first to discover the right uid.
|
|
2416
|
+
direction: "downstream" (callers — break if `uid` changes) |
|
|
2417
|
+
"upstream" (deps `uid` calls/imports) | "both".
|
|
2418
|
+
depth: BFS hop limit (default 3).
|
|
2419
|
+
confidence_min: Drop edges below this score (default 0.3, matching the function + HTTP route).
|
|
2420
|
+
visit_limit: BFS node-visit cap (1..50000, default 500). Raise when meta.walk_truncated is true.
|
|
2421
|
+
"""
|
|
2422
|
+
return _graph_tools.cos_graph_impact(
|
|
2423
|
+
uid,
|
|
2424
|
+
direction=str(direction),
|
|
2425
|
+
depth=int(depth),
|
|
2426
|
+
confidence_min=float(confidence_min),
|
|
2427
|
+
visit_limit=int(visit_limit),
|
|
2428
|
+
)
|
|
2429
|
+
|
|
2430
|
+
@mcp.tool(
|
|
2431
|
+
name="cos_graph_detect_changes",
|
|
2432
|
+
annotations={
|
|
2433
|
+
"title": "Graph Pre-Commit Self-Review",
|
|
2434
|
+
"readOnlyHint": True,
|
|
2435
|
+
"destructiveHint": False,
|
|
2436
|
+
"idempotentHint": True,
|
|
2437
|
+
"openWorldHint": False,
|
|
2438
|
+
},
|
|
2439
|
+
)
|
|
2440
|
+
@safe_tool
|
|
2441
|
+
def cos_graph_detect_changes_tool(
|
|
2442
|
+
files: str = "",
|
|
2443
|
+
scope: str = "working",
|
|
2444
|
+
analyze_downstream: bool = True,
|
|
2445
|
+
) -> str:
|
|
2446
|
+
"""Map changed files to affected symbols + downstream tasks + risk level.
|
|
2447
|
+
|
|
2448
|
+
Args:
|
|
2449
|
+
files: Comma-separated file paths (empty → echo empty envelope).
|
|
2450
|
+
scope: Label only; "working" | "staged" | "HEAD~1..HEAD".
|
|
2451
|
+
analyze_downstream: Walk transitive blast radius.
|
|
2452
|
+
"""
|
|
2453
|
+
return _graph_tools.cos_graph_detect_changes(
|
|
2454
|
+
scope=str(scope),
|
|
2455
|
+
files=_csv(files),
|
|
2456
|
+
analyze_downstream=bool(analyze_downstream),
|
|
2457
|
+
)
|
|
2458
|
+
|
|
2459
|
+
@mcp.tool(
|
|
2460
|
+
name="cos_graph_trace",
|
|
2461
|
+
annotations={
|
|
2462
|
+
"title": "Graph Execution Trace",
|
|
2463
|
+
"readOnlyHint": True,
|
|
2464
|
+
"destructiveHint": False,
|
|
2465
|
+
"idempotentHint": True,
|
|
2466
|
+
"openWorldHint": False,
|
|
2467
|
+
},
|
|
2468
|
+
)
|
|
2469
|
+
@safe_tool
|
|
2470
|
+
def cos_graph_trace_tool(
|
|
2471
|
+
entry_uid: str,
|
|
2472
|
+
terminals: str = "return,exception",
|
|
2473
|
+
max_steps: int = 50,
|
|
2474
|
+
include_external: bool = False,
|
|
2475
|
+
) -> str:
|
|
2476
|
+
"""Forward execution walk from `entry_uid` until terminals.
|
|
2477
|
+
|
|
2478
|
+
Args:
|
|
2479
|
+
entry_uid: Function/method uid to start from, e.g.
|
|
2480
|
+
``code:function:core/foo.py::bar``. Raw paths or names are
|
|
2481
|
+
auto-resolved (file → ``code:file:`` then entry-point heuristic).
|
|
2482
|
+
Run ``cos_graph_query`` first if unsure.
|
|
2483
|
+
terminals: Comma-separated edge labels that stop the walk.
|
|
2484
|
+
max_steps: Hard cap on emitted steps.
|
|
2485
|
+
"""
|
|
2486
|
+
return _graph_tools.cos_graph_trace(
|
|
2487
|
+
entry_uid,
|
|
2488
|
+
terminals=tuple(_csv(terminals) or ("return", "exception")),
|
|
2489
|
+
max_steps=int(max_steps),
|
|
2490
|
+
include_external=bool(include_external),
|
|
2491
|
+
)
|
|
2492
|
+
|
|
2493
|
+
@mcp.tool(
|
|
2494
|
+
name="cos_graph_similar",
|
|
2495
|
+
annotations={
|
|
2496
|
+
"title": "Graph Semantic Similarity",
|
|
2497
|
+
"readOnlyHint": True,
|
|
2498
|
+
"destructiveHint": False,
|
|
2499
|
+
"idempotentHint": True,
|
|
2500
|
+
"openWorldHint": False,
|
|
2501
|
+
},
|
|
2502
|
+
)
|
|
2503
|
+
@safe_tool
|
|
2504
|
+
def cos_graph_similar_tool(
|
|
2505
|
+
uid: str,
|
|
2506
|
+
top_k: int = 5,
|
|
2507
|
+
confidence_min: float = 0.5,
|
|
2508
|
+
) -> str:
|
|
2509
|
+
"""Return the top-K nodes most similar to `uid` (difflib baseline).
|
|
2510
|
+
|
|
2511
|
+
Args:
|
|
2512
|
+
uid: Fully-qualified node uid (see ``cos_graph_impact`` for
|
|
2513
|
+
scheme). Raw repo paths are auto-resolved to
|
|
2514
|
+
``code:file:`` / ``doc:file:`` / ``folder:``.
|
|
2515
|
+
top_k: Number of similar nodes to return.
|
|
2516
|
+
confidence_min: Minimum similarity score (0.0–1.0).
|
|
2517
|
+
"""
|
|
2518
|
+
return _graph_tools.cos_graph_similar(
|
|
2519
|
+
uid,
|
|
2520
|
+
top_k=int(top_k),
|
|
2521
|
+
confidence_min=float(confidence_min),
|
|
2522
|
+
)
|
|
2523
|
+
|
|
2524
|
+
@mcp.tool(
|
|
2525
|
+
name="cos_graph_search",
|
|
2526
|
+
annotations={
|
|
2527
|
+
"title": "Graph Semantic Search (by description)",
|
|
2528
|
+
"readOnlyHint": True,
|
|
2529
|
+
"destructiveHint": False,
|
|
2530
|
+
"idempotentHint": True,
|
|
2531
|
+
"openWorldHint": False,
|
|
2532
|
+
},
|
|
2533
|
+
)
|
|
2534
|
+
@safe_tool
|
|
2535
|
+
def cos_graph_search_tool(
|
|
2536
|
+
query: str,
|
|
2537
|
+
top_k: int = 10,
|
|
2538
|
+
) -> str:
|
|
2539
|
+
"""Find code symbols from a NATURAL-LANGUAGE description (semantic + lexical + centrality). For a KNOWN name / path / uid, use cos_graph_query instead.
|
|
2540
|
+
|
|
2541
|
+
Args:
|
|
2542
|
+
query: Natural-language or code-ish query (e.g. "validate jwt token").
|
|
2543
|
+
top_k: Number of results to return (1–50).
|
|
2544
|
+
"""
|
|
2545
|
+
return _graph_tools.cos_graph_search(query, top_k=int(top_k))
|
|
2546
|
+
|
|
2547
|
+
@mcp.tool(
|
|
2548
|
+
name="cos_graph_references",
|
|
2549
|
+
annotations={
|
|
2550
|
+
"title": "Graph Inbound References",
|
|
2551
|
+
"readOnlyHint": True,
|
|
2552
|
+
"destructiveHint": False,
|
|
2553
|
+
"idempotentHint": True,
|
|
2554
|
+
"openWorldHint": False,
|
|
2555
|
+
},
|
|
2556
|
+
)
|
|
2557
|
+
@safe_tool
|
|
2558
|
+
def cos_graph_references_tool(
|
|
2559
|
+
uid: str,
|
|
2560
|
+
kinds: str = "",
|
|
2561
|
+
limit: int = 100,
|
|
2562
|
+
) -> str:
|
|
2563
|
+
"""List inbound edges — "who references this?".
|
|
2564
|
+
|
|
2565
|
+
Args:
|
|
2566
|
+
uid: Fully-qualified node uid. Scheme: ``code:file:<path>`` |
|
|
2567
|
+
``code:function:<path>::<name>`` | ``code:class:<path>::<name>`` |
|
|
2568
|
+
``code:module:<dotted>`` | ``doc:file:<path>`` | ``folder:<path>``.
|
|
2569
|
+
Raw repo paths are auto-resolved.
|
|
2570
|
+
kinds: Comma-separated edge types. Empty string (default)
|
|
2571
|
+
picks edge types automatically per node-kind — class
|
|
2572
|
+
nodes get ``constructs+has_param_type+is_decorated_by+inherits_from``,
|
|
2573
|
+
function/method get ``calls+accesses_field+imports``, files
|
|
2574
|
+
get ``imports+links_to+references_doc+contains``. R4-02.
|
|
2575
|
+
limit: Max edges returned (default 100).
|
|
2576
|
+
"""
|
|
2577
|
+
parsed = tuple(_csv(kinds) or ())
|
|
2578
|
+
return _graph_tools.cos_graph_references(
|
|
2579
|
+
uid,
|
|
2580
|
+
kinds=parsed if parsed else None,
|
|
2581
|
+
limit=int(limit),
|
|
2582
|
+
)
|
|
2583
|
+
|
|
2584
|
+
@mcp.tool(
|
|
2585
|
+
name="cos_graph_path",
|
|
2586
|
+
annotations={
|
|
2587
|
+
"title": "Graph Shortest Path",
|
|
2588
|
+
"readOnlyHint": True,
|
|
2589
|
+
"destructiveHint": False,
|
|
2590
|
+
"idempotentHint": True,
|
|
2591
|
+
"openWorldHint": False,
|
|
2592
|
+
},
|
|
2593
|
+
)
|
|
2594
|
+
@safe_tool
|
|
2595
|
+
def cos_graph_path_tool(
|
|
2596
|
+
source_uid: str,
|
|
2597
|
+
target_uid: str,
|
|
2598
|
+
max_hops: int = 5,
|
|
2599
|
+
) -> str:
|
|
2600
|
+
"""Shortest path between two nodes (either direction).
|
|
2601
|
+
|
|
2602
|
+
Args:
|
|
2603
|
+
source_uid: Origin uid (auto-resolves raw paths; see
|
|
2604
|
+
``cos_graph_impact`` for the scheme).
|
|
2605
|
+
target_uid: Destination uid (same rules as ``source_uid``).
|
|
2606
|
+
max_hops: BFS depth limit (default 5).
|
|
2607
|
+
"""
|
|
2608
|
+
return _graph_tools.cos_graph_path(
|
|
2609
|
+
source_uid,
|
|
2610
|
+
target_uid,
|
|
2611
|
+
max_hops=int(max_hops),
|
|
2612
|
+
)
|
|
2613
|
+
|
|
2614
|
+
@mcp.tool(
|
|
2615
|
+
name="cos_graph_export",
|
|
2616
|
+
annotations={
|
|
2617
|
+
"title": "Graph Subgraph Export",
|
|
2618
|
+
"readOnlyHint": True,
|
|
2619
|
+
"destructiveHint": False,
|
|
2620
|
+
"idempotentHint": True,
|
|
2621
|
+
"openWorldHint": False,
|
|
2622
|
+
},
|
|
2623
|
+
)
|
|
2624
|
+
@safe_tool
|
|
2625
|
+
def cos_graph_export_tool(
|
|
2626
|
+
format: str = "json",
|
|
2627
|
+
root_uid: str = "",
|
|
2628
|
+
edge_types: str = "",
|
|
2629
|
+
max_nodes: int = 500,
|
|
2630
|
+
include_spine: bool = False,
|
|
2631
|
+
mode: str = "auto",
|
|
2632
|
+
exclude_kinds: str = "__default__",
|
|
2633
|
+
) -> str:
|
|
2634
|
+
"""Export a subgraph as json | mermaid | dot.
|
|
2635
|
+
|
|
2636
|
+
Args:
|
|
2637
|
+
format: Output format (``json`` / ``mermaid`` / ``dot``).
|
|
2638
|
+
root_uid: Optional seed; empty walks the edge table.
|
|
2639
|
+
edge_types: Comma-separated edge filter (empty = all).
|
|
2640
|
+
max_nodes: Hard cap on node count.
|
|
2641
|
+
include_spine: S3 — also include the CONTAINS ancestor chain.
|
|
2642
|
+
mode: TASK-141 view-mode blend when no root is pinned —
|
|
2643
|
+
``auto`` (semantic + contains, default), ``containment``,
|
|
2644
|
+
``dependencies``, or ``processes``.
|
|
2645
|
+
exclude_kinds: Comma-separated noise kinds to drop. Sentinel
|
|
2646
|
+
``__default__`` (default) applies the built-in noise list;
|
|
2647
|
+
empty string disables filtering.
|
|
2648
|
+
"""
|
|
2649
|
+
if exclude_kinds == "__default__":
|
|
2650
|
+
ek = None
|
|
2651
|
+
elif exclude_kinds == "":
|
|
2652
|
+
ek = []
|
|
2653
|
+
else:
|
|
2654
|
+
ek = list(_csv(exclude_kinds) or ())
|
|
2655
|
+
return _graph_tools.cos_graph_export(
|
|
2656
|
+
format=str(format),
|
|
2657
|
+
root_uid=root_uid or None,
|
|
2658
|
+
edge_types=_csv(edge_types),
|
|
2659
|
+
max_nodes=int(max_nodes),
|
|
2660
|
+
include_spine=bool(include_spine),
|
|
2661
|
+
mode=str(mode),
|
|
2662
|
+
exclude_kinds=ek,
|
|
2663
|
+
)
|
|
2664
|
+
|
|
2665
|
+
@mcp.tool(
|
|
2666
|
+
name="cos_graph_rename_plan",
|
|
2667
|
+
annotations={
|
|
2668
|
+
"title": "Graph Rename Plan",
|
|
2669
|
+
"readOnlyHint": True,
|
|
2670
|
+
"destructiveHint": False,
|
|
2671
|
+
"idempotentHint": True,
|
|
2672
|
+
"openWorldHint": False,
|
|
2673
|
+
},
|
|
2674
|
+
)
|
|
2675
|
+
@safe_tool
|
|
2676
|
+
def cos_graph_rename_plan_tool(
|
|
2677
|
+
uid: str,
|
|
2678
|
+
new_name: str,
|
|
2679
|
+
check_strings: bool = True,
|
|
2680
|
+
) -> str:
|
|
2681
|
+
"""Plan a rename — call-sites, docs, tests, strings, risk.
|
|
2682
|
+
|
|
2683
|
+
Args:
|
|
2684
|
+
uid: Symbol to rename. Scheme: ``code:function:<path>::<name>`` |
|
|
2685
|
+
``code:class:<path>::<name>`` | ``code:module:<dotted>``.
|
|
2686
|
+
Raw paths are auto-resolved when applicable.
|
|
2687
|
+
new_name: Replacement symbol name.
|
|
2688
|
+
check_strings: Also scan string literals for the old name.
|
|
2689
|
+
"""
|
|
2690
|
+
return _graph_tools.cos_graph_rename_plan(
|
|
2691
|
+
uid,
|
|
2692
|
+
new_name,
|
|
2693
|
+
check_strings=bool(check_strings),
|
|
2694
|
+
)
|
|
2695
|
+
|
|
2696
|
+
@mcp.tool(
|
|
2697
|
+
name="cos_graph_contracts",
|
|
2698
|
+
annotations={
|
|
2699
|
+
"title": "Graph API Contracts",
|
|
2700
|
+
"readOnlyHint": True,
|
|
2701
|
+
"destructiveHint": False,
|
|
2702
|
+
"idempotentHint": True,
|
|
2703
|
+
"openWorldHint": False,
|
|
2704
|
+
},
|
|
2705
|
+
)
|
|
2706
|
+
@safe_tool
|
|
2707
|
+
def cos_graph_contracts_tool(
|
|
2708
|
+
scope: str = "all",
|
|
2709
|
+
kinds: str = "http,mcp,grpc,event,websocket",
|
|
2710
|
+
include_test_sources: bool = False,
|
|
2711
|
+
) -> str:
|
|
2712
|
+
"""Enumerate every handler declared in the graph (HTTP / MCP / gRPC / events / WS)."""
|
|
2713
|
+
return _graph_tools.cos_graph_contracts(
|
|
2714
|
+
scope=str(scope),
|
|
2715
|
+
kinds=tuple(_csv(kinds) or ("http", "mcp", "grpc", "event", "websocket")),
|
|
2716
|
+
include_test_sources=bool(include_test_sources),
|
|
2717
|
+
)
|
|
2718
|
+
|
|
2719
|
+
@mcp.tool(
|
|
2720
|
+
name="cos_graph_entrypoints",
|
|
2721
|
+
annotations={
|
|
2722
|
+
"title": "Graph Entry Points (Scored)",
|
|
2723
|
+
"readOnlyHint": True,
|
|
2724
|
+
"destructiveHint": False,
|
|
2725
|
+
"idempotentHint": True,
|
|
2726
|
+
"openWorldHint": False,
|
|
2727
|
+
},
|
|
2728
|
+
)
|
|
2729
|
+
@safe_tool
|
|
2730
|
+
def cos_graph_entrypoints_tool(
|
|
2731
|
+
top: int = 20,
|
|
2732
|
+
kind: str = "",
|
|
2733
|
+
min_score: float = 0.05,
|
|
2734
|
+
diversify: bool = True,
|
|
2735
|
+
) -> str:
|
|
2736
|
+
"""Top-N scored entry points (main / cli / http / cron / test) — TASK-081."""
|
|
2737
|
+
return _graph_tools.cos_graph_entrypoints(
|
|
2738
|
+
top=int(top),
|
|
2739
|
+
kind=(kind or None),
|
|
2740
|
+
min_score=float(min_score),
|
|
2741
|
+
diversify=bool(diversify),
|
|
2742
|
+
)
|
|
2743
|
+
|
|
2744
|
+
@mcp.tool(
|
|
2745
|
+
name="cos_graph_communities",
|
|
2746
|
+
annotations={
|
|
2747
|
+
"title": "Graph Communities / Processes (Louvain)",
|
|
2748
|
+
"readOnlyHint": True,
|
|
2749
|
+
"destructiveHint": False,
|
|
2750
|
+
"idempotentHint": True,
|
|
2751
|
+
"openWorldHint": False,
|
|
2752
|
+
},
|
|
2753
|
+
)
|
|
2754
|
+
@safe_tool
|
|
2755
|
+
def cos_graph_communities_tool(
|
|
2756
|
+
top: int = 50,
|
|
2757
|
+
min_size: int = 2,
|
|
2758
|
+
max_members: int = 10,
|
|
2759
|
+
) -> str:
|
|
2760
|
+
"""Louvain process clusters — response key is `processes` (not `communities`)."""
|
|
2761
|
+
return _graph_tools.cos_graph_communities(
|
|
2762
|
+
top=int(top),
|
|
2763
|
+
min_size=int(min_size),
|
|
2764
|
+
max_members=int(max_members),
|
|
2765
|
+
)
|
|
2766
|
+
|
|
2767
|
+
@mcp.tool(
|
|
2768
|
+
name="cos_graph_resolve",
|
|
2769
|
+
annotations={
|
|
2770
|
+
"title": "Graph UID Resolver (NL → canonical uid)",
|
|
2771
|
+
"readOnlyHint": True,
|
|
2772
|
+
"destructiveHint": False,
|
|
2773
|
+
"idempotentHint": True,
|
|
2774
|
+
"openWorldHint": False,
|
|
2775
|
+
},
|
|
2776
|
+
)
|
|
2777
|
+
@safe_tool
|
|
2778
|
+
def cos_graph_resolve_tool(
|
|
2779
|
+
q: str,
|
|
2780
|
+
kinds: str = "",
|
|
2781
|
+
top: int = 10,
|
|
2782
|
+
) -> str:
|
|
2783
|
+
"""Resolve a natural-language label, path, or partial uid to canonical uids.
|
|
2784
|
+
|
|
2785
|
+
Use this BEFORE other cos_graph_* tools when you don't know the exact uid.
|
|
2786
|
+
Tries: direct uid → path/qualname → FTS5 full-text → LIKE fallback.
|
|
2787
|
+
|
|
2788
|
+
UID scheme:
|
|
2789
|
+
code:file:<path> · code:function:<path>::<name> · code:class:<path>::<name>
|
|
2790
|
+
code:method:<path>::<class>.<name> · code:module:<dotted>
|
|
2791
|
+
doc:file:<path> · doc:heading:<path>#<slug>:<level> · folder:<path>
|
|
2792
|
+
|
|
2793
|
+
Args:
|
|
2794
|
+
q: Natural language ("the dispatcher function"), label ("ClaudeSDKDispatcher"),
|
|
2795
|
+
path ("adapters/claude/sdk_dispatcher.py"), or qualname ("Class.method").
|
|
2796
|
+
kinds: Comma-separated kind filter (e.g. "function,method,class"). Empty = all.
|
|
2797
|
+
top: Max results (default 10).
|
|
2798
|
+
|
|
2799
|
+
Returns:
|
|
2800
|
+
JSON envelope with `results` (ranked list of {uid, kind, label, …}) and
|
|
2801
|
+
`strategy` (which resolution path matched).
|
|
2802
|
+
"""
|
|
2803
|
+
return _graph_tools.cos_graph_resolve(
|
|
2804
|
+
q,
|
|
2805
|
+
kinds=_csv(kinds) or None,
|
|
2806
|
+
top=int(top),
|
|
2807
|
+
)
|
|
2808
|
+
|
|
2809
|
+
@mcp.tool(
|
|
2810
|
+
name="cos_graph_centrality",
|
|
2811
|
+
annotations={
|
|
2812
|
+
"title": "Graph Centrality (degree / betweenness)",
|
|
2813
|
+
"readOnlyHint": True,
|
|
2814
|
+
"destructiveHint": False,
|
|
2815
|
+
"idempotentHint": True,
|
|
2816
|
+
"openWorldHint": False,
|
|
2817
|
+
},
|
|
2818
|
+
)
|
|
2819
|
+
@safe_tool
|
|
2820
|
+
def cos_graph_centrality_tool(
|
|
2821
|
+
metric: str = "degree",
|
|
2822
|
+
top: int = 20,
|
|
2823
|
+
kind: str = "",
|
|
2824
|
+
) -> str:
|
|
2825
|
+
"""Hub detection — surface high-degree (or high-betweenness) nodes.
|
|
2826
|
+
|
|
2827
|
+
Use to identify chokepoints / refactor priorities / nodes that demand
|
|
2828
|
+
extra review.
|
|
2829
|
+
|
|
2830
|
+
Args:
|
|
2831
|
+
metric: "degree" (cheap, default) or "betweenness" (expensive).
|
|
2832
|
+
top: Max nodes returned (default 20).
|
|
2833
|
+
kind: Optional kind filter (e.g. "function", "class"). Empty = all.
|
|
2834
|
+
|
|
2835
|
+
Returns:
|
|
2836
|
+
JSON envelope with `nodes` ranked by centrality score.
|
|
2837
|
+
"""
|
|
2838
|
+
return _graph_tools.cos_graph_centrality(
|
|
2839
|
+
metric=metric,
|
|
2840
|
+
top=int(top),
|
|
2841
|
+
kind=kind or None,
|
|
2842
|
+
)
|
|
2843
|
+
|
|
2844
|
+
@mcp.tool(
|
|
2845
|
+
name="cos_graph_ranking",
|
|
2846
|
+
annotations={
|
|
2847
|
+
"title": "Graph PageRank (importance / personalised)",
|
|
2848
|
+
"readOnlyHint": True,
|
|
2849
|
+
"destructiveHint": False,
|
|
2850
|
+
"idempotentHint": True,
|
|
2851
|
+
"openWorldHint": False,
|
|
2852
|
+
},
|
|
2853
|
+
)
|
|
2854
|
+
@safe_tool
|
|
2855
|
+
def cos_graph_ranking_tool(
|
|
2856
|
+
query: str = "",
|
|
2857
|
+
top: int = 20,
|
|
2858
|
+
kind: str = "",
|
|
2859
|
+
damping: float = 0.85,
|
|
2860
|
+
iterations: int = 30,
|
|
2861
|
+
) -> str:
|
|
2862
|
+
"""PageRank — node importance, optionally personalised by query.
|
|
2863
|
+
|
|
2864
|
+
Use for: knowledge condensation (top-N canonical concepts),
|
|
2865
|
+
query-personalised search ranking, documentation sourcing.
|
|
2866
|
+
|
|
2867
|
+
Args:
|
|
2868
|
+
query: Optional personalisation query ("auth", "graph backend").
|
|
2869
|
+
Empty = global PageRank.
|
|
2870
|
+
top: Max nodes returned (default 20).
|
|
2871
|
+
kind: Optional kind filter. Empty = all.
|
|
2872
|
+
damping: PageRank damping factor (default 0.85).
|
|
2873
|
+
iterations: Power-iteration count (default 30).
|
|
2874
|
+
|
|
2875
|
+
Returns:
|
|
2876
|
+
JSON envelope with `nodes` ranked by PageRank score.
|
|
2877
|
+
"""
|
|
2878
|
+
return _graph_tools.cos_graph_ranking(
|
|
2879
|
+
query=query or None,
|
|
2880
|
+
top=int(top),
|
|
2881
|
+
kind=kind or None,
|
|
2882
|
+
damping=float(damping),
|
|
2883
|
+
iterations=int(iterations),
|
|
2884
|
+
)
|
|
2885
|
+
|
|
2886
|
+
@mcp.tool(
|
|
2887
|
+
name="cos_graph_cycles",
|
|
2888
|
+
annotations={
|
|
2889
|
+
"title": "Graph Circular Dependencies (SCC)",
|
|
2890
|
+
"readOnlyHint": True,
|
|
2891
|
+
"destructiveHint": False,
|
|
2892
|
+
"idempotentHint": True,
|
|
2893
|
+
"openWorldHint": False,
|
|
2894
|
+
},
|
|
2895
|
+
)
|
|
2896
|
+
@safe_tool
|
|
2897
|
+
def cos_graph_cycles_tool(
|
|
2898
|
+
scope: str = "imports",
|
|
2899
|
+
top: int = 20,
|
|
2900
|
+
min_size: int = 2,
|
|
2901
|
+
) -> str:
|
|
2902
|
+
"""Detect circular dependencies as strongly-connected components.
|
|
2903
|
+
|
|
2904
|
+
Args:
|
|
2905
|
+
scope: "imports" (module-level circular deps, the design smell) or
|
|
2906
|
+
"calls" (function cycles incl. legitimate mutual recursion).
|
|
2907
|
+
top: Max cycles returned (default 20).
|
|
2908
|
+
min_size: Minimum SCC size to report (default 2).
|
|
2909
|
+
|
|
2910
|
+
Returns:
|
|
2911
|
+
JSON envelope with `cycles` (each {size, members}) + total_count.
|
|
2912
|
+
"""
|
|
2913
|
+
return _graph_tools.cos_graph_cycles(
|
|
2914
|
+
scope=str(scope),
|
|
2915
|
+
top=int(top),
|
|
2916
|
+
min_size=int(min_size),
|
|
2917
|
+
)
|
|
2918
|
+
|
|
2919
|
+
@mcp.tool(
|
|
2920
|
+
name="cos_graph_dead_code",
|
|
2921
|
+
annotations={
|
|
2922
|
+
"title": "Graph Dead-Code Candidates",
|
|
2923
|
+
"readOnlyHint": True,
|
|
2924
|
+
"destructiveHint": False,
|
|
2925
|
+
"idempotentHint": True,
|
|
2926
|
+
"openWorldHint": False,
|
|
2927
|
+
},
|
|
2928
|
+
)
|
|
2929
|
+
@safe_tool
|
|
2930
|
+
def cos_graph_dead_code_tool(
|
|
2931
|
+
kind: str = "",
|
|
2932
|
+
top: int = 50,
|
|
2933
|
+
include_tests: bool = False,
|
|
2934
|
+
) -> str:
|
|
2935
|
+
"""List in-repo symbols with zero non-test inbound references (dead-code candidates).
|
|
2936
|
+
|
|
2937
|
+
Surfaces functions / methods / classes that nothing (outside tests)
|
|
2938
|
+
calls, constructs, subclasses, or type-references — the inverse of
|
|
2939
|
+
centrality. Candidates only: dynamic-dispatch / CLI-registered /
|
|
2940
|
+
externally-called symbols may appear; verify with cos_graph_references
|
|
2941
|
+
before deleting.
|
|
2942
|
+
|
|
2943
|
+
Args:
|
|
2944
|
+
kind: Optional filter — function | method | class. Empty = all three.
|
|
2945
|
+
top: Max candidates returned (default 50, max 500).
|
|
2946
|
+
include_tests: Count test-sourced edges + include test files (default False).
|
|
2947
|
+
|
|
2948
|
+
Returns:
|
|
2949
|
+
JSON envelope with `dead` (list) + `total_count`.
|
|
2950
|
+
"""
|
|
2951
|
+
return _graph_tools.cos_graph_dead_code(
|
|
2952
|
+
kind=kind or "",
|
|
2953
|
+
top=int(top),
|
|
2954
|
+
include_tests=bool(include_tests),
|
|
2955
|
+
)
|
|
2956
|
+
|
|
2957
|
+
@mcp.tool(
|
|
2958
|
+
name="cos_graph_test_gap",
|
|
2959
|
+
annotations={
|
|
2960
|
+
"title": "Graph Test-Gap (untested symbols)",
|
|
2961
|
+
"readOnlyHint": True,
|
|
2962
|
+
"destructiveHint": False,
|
|
2963
|
+
"idempotentHint": True,
|
|
2964
|
+
"openWorldHint": False,
|
|
2965
|
+
},
|
|
2966
|
+
)
|
|
2967
|
+
@safe_tool
|
|
2968
|
+
def cos_graph_test_gap_tool(
|
|
2969
|
+
kind: str = "",
|
|
2970
|
+
top: int = 50,
|
|
2971
|
+
) -> str:
|
|
2972
|
+
"""List prod function/method/class with zero inbound edge from any test (untested symbols).
|
|
2973
|
+
|
|
2974
|
+
Candidates only: indirect exercise (CLI / fixtures / dynamic dispatch)
|
|
2975
|
+
may not appear as a graph edge. Shell excluded (no call-graph).
|
|
2976
|
+
|
|
2977
|
+
Args:
|
|
2978
|
+
kind: Optional filter — function | method | class. Empty = all three.
|
|
2979
|
+
top: Max returned (default 50, max 500).
|
|
2980
|
+
|
|
2981
|
+
Returns:
|
|
2982
|
+
JSON envelope with `untested` (list) + total_count.
|
|
2983
|
+
"""
|
|
2984
|
+
return _graph_tools.cos_graph_test_gap(kind=kind or "", top=int(top))
|
|
2985
|
+
|
|
2986
|
+
@mcp.tool(
|
|
2987
|
+
name="cos_graph_diff",
|
|
2988
|
+
annotations={
|
|
2989
|
+
"title": "Graph Diff (git revision blast-radius)",
|
|
2990
|
+
"readOnlyHint": True,
|
|
2991
|
+
"destructiveHint": False,
|
|
2992
|
+
"idempotentHint": True,
|
|
2993
|
+
"openWorldHint": False,
|
|
2994
|
+
},
|
|
2995
|
+
)
|
|
2996
|
+
@safe_tool
|
|
2997
|
+
def cos_graph_diff_tool(
|
|
2998
|
+
base: str = "HEAD~1",
|
|
2999
|
+
head: str = "HEAD",
|
|
3000
|
+
analyze_downstream: bool = True,
|
|
3001
|
+
) -> str:
|
|
3002
|
+
"""Graph blast-radius of a git revision range (base..head).
|
|
3003
|
+
|
|
3004
|
+
Resolves changed files via `git diff --name-only base..head`, then maps
|
|
3005
|
+
them to affected symbols + downstream consumers + risk (PR/review view).
|
|
3006
|
+
|
|
3007
|
+
Args:
|
|
3008
|
+
base: Base git revision (default HEAD~1).
|
|
3009
|
+
head: Head git revision (default HEAD).
|
|
3010
|
+
analyze_downstream: Walk transitive consumers (default True).
|
|
3011
|
+
|
|
3012
|
+
Returns:
|
|
3013
|
+
JSON envelope with range, files, symbols, downstream_consumers, risk_level.
|
|
3014
|
+
"""
|
|
3015
|
+
return _graph_tools.cos_graph_diff(
|
|
3016
|
+
base=str(base),
|
|
3017
|
+
head=str(head),
|
|
3018
|
+
analyze_downstream=bool(analyze_downstream),
|
|
3019
|
+
)
|
|
3020
|
+
|
|
3021
|
+
@mcp.tool(
|
|
3022
|
+
name="cos_graph_doctor",
|
|
3023
|
+
annotations={
|
|
3024
|
+
"title": "Graph Health Doctor",
|
|
3025
|
+
"readOnlyHint": True,
|
|
3026
|
+
"destructiveHint": False,
|
|
3027
|
+
"idempotentHint": True,
|
|
3028
|
+
"openWorldHint": False,
|
|
3029
|
+
},
|
|
3030
|
+
)
|
|
3031
|
+
@safe_tool
|
|
3032
|
+
def cos_graph_doctor_tool(
|
|
3033
|
+
fix: bool = False,
|
|
3034
|
+
) -> str:
|
|
3035
|
+
"""Graph health snapshot — orphans, dangling edges, duplicates, backend status.
|
|
3036
|
+
|
|
3037
|
+
Call when graph queries return nothing or `meta.backend_fallback=true`.
|
|
3038
|
+
|
|
3039
|
+
Args:
|
|
3040
|
+
fix: If True, attempt safe repairs (delete dangling edges). Default False
|
|
3041
|
+
— use the report-only mode to see what would change first.
|
|
3042
|
+
|
|
3043
|
+
Returns:
|
|
3044
|
+
JSON envelope with `healthy` boolean, `issues` list, `stats` dict.
|
|
3045
|
+
"""
|
|
3046
|
+
return _graph_tools.cos_graph_doctor(
|
|
3047
|
+
fix=bool(fix),
|
|
3048
|
+
)
|
|
3049
|
+
|
|
3050
|
+
else:
|
|
3051
|
+
# Deterministic unavailable responses so agents still see a valid envelope.
|
|
3052
|
+
for _name in (
|
|
3053
|
+
"cos_graph_query",
|
|
3054
|
+
"cos_graph_resolve",
|
|
3055
|
+
"cos_graph_context",
|
|
3056
|
+
"cos_graph_impact",
|
|
3057
|
+
"cos_graph_detect_changes",
|
|
3058
|
+
"cos_graph_trace",
|
|
3059
|
+
"cos_graph_similar",
|
|
3060
|
+
"cos_graph_search",
|
|
3061
|
+
"cos_graph_references",
|
|
3062
|
+
"cos_graph_path",
|
|
3063
|
+
"cos_graph_export",
|
|
3064
|
+
"cos_graph_rename_plan",
|
|
3065
|
+
"cos_graph_contracts",
|
|
3066
|
+
"cos_graph_entrypoints",
|
|
3067
|
+
"cos_graph_communities",
|
|
3068
|
+
"cos_graph_centrality",
|
|
3069
|
+
"cos_graph_ranking",
|
|
3070
|
+
"cos_graph_cycles",
|
|
3071
|
+
"cos_graph_dead_code",
|
|
3072
|
+
"cos_graph_test_gap",
|
|
3073
|
+
"cos_graph_diff",
|
|
3074
|
+
"cos_graph_doctor",
|
|
3075
|
+
):
|
|
3076
|
+
|
|
3077
|
+
def _make_stub(tool_name: str):
|
|
3078
|
+
@mcp.tool(
|
|
3079
|
+
name=tool_name,
|
|
3080
|
+
annotations={"title": f"{tool_name} (unavailable)", "readOnlyHint": True},
|
|
3081
|
+
)
|
|
3082
|
+
@safe_tool
|
|
3083
|
+
def _stub(*_args: object, **_kwargs: object) -> str:
|
|
3084
|
+
return _graph_unavailable()
|
|
3085
|
+
|
|
3086
|
+
return _stub
|
|
3087
|
+
|
|
3088
|
+
_make_stub(_name)
|
|
3089
|
+
|
|
3090
|
+
|
|
3091
|
+
# ---------------------------------------------------------------------------
|
|
3092
|
+
# Entry point
|
|
3093
|
+
# ---------------------------------------------------------------------------
|
|
3094
|
+
def _run_self_test() -> bool:
|
|
3095
|
+
"""Quick self-test: verify DB is reachable and health tool works.
|
|
3096
|
+
|
|
3097
|
+
Walks the MCP envelope (docs/engineering/mcp-error-envelope.md) — asserts
|
|
3098
|
+
`ok: true` then drills into `data` for the actual health stats.
|
|
3099
|
+
"""
|
|
3100
|
+
logger.info("Running self-test...")
|
|
3101
|
+
envelope = json.loads(thinking_os_health())
|
|
3102
|
+
|
|
3103
|
+
if not envelope.get("ok"):
|
|
3104
|
+
logger.error("FAIL: health returned error envelope: %s", envelope.get("error"))
|
|
3105
|
+
return False
|
|
3106
|
+
|
|
3107
|
+
data = envelope["data"]
|
|
3108
|
+
checks_passed = True
|
|
3109
|
+
|
|
3110
|
+
if "schema_version" not in data:
|
|
3111
|
+
logger.error("FAIL: schema_version missing from health response")
|
|
3112
|
+
checks_passed = False
|
|
3113
|
+
elif data["schema_version"] < 1:
|
|
3114
|
+
logger.error("FAIL: schema_version is %d, expected >= 1", data["schema_version"])
|
|
3115
|
+
checks_passed = False
|
|
3116
|
+
|
|
3117
|
+
if "tables" not in data:
|
|
3118
|
+
logger.error("FAIL: tables missing from health response")
|
|
3119
|
+
checks_passed = False
|
|
3120
|
+
else:
|
|
3121
|
+
expected_tables = [
|
|
3122
|
+
"task_outcomes",
|
|
3123
|
+
"agent_metrics",
|
|
3124
|
+
"learned_patterns",
|
|
3125
|
+
"observations",
|
|
3126
|
+
"session_summaries",
|
|
3127
|
+
]
|
|
3128
|
+
for table in expected_tables:
|
|
3129
|
+
if table not in data["tables"]:
|
|
3130
|
+
logger.error("FAIL: table '%s' missing from stats", table)
|
|
3131
|
+
checks_passed = False
|
|
3132
|
+
elif data["tables"][table] is None:
|
|
3133
|
+
logger.error("FAIL: table '%s' does not exist in DB", table)
|
|
3134
|
+
checks_passed = False
|
|
3135
|
+
|
|
3136
|
+
if checks_passed:
|
|
3137
|
+
logger.info("PASS: all self-test checks passed")
|
|
3138
|
+
logger.info("Stats: %s", json.dumps(data, indent=2))
|
|
3139
|
+
return checks_passed
|
|
3140
|
+
|
|
3141
|
+
|
|
3142
|
+
def main() -> None:
|
|
3143
|
+
"""Entry point — handles --test flag or starts MCP stdio server."""
|
|
3144
|
+
if "--test" in sys.argv:
|
|
3145
|
+
success = _run_self_test()
|
|
3146
|
+
sys.exit(0 if success else 1)
|
|
3147
|
+
else:
|
|
3148
|
+
logger.info("Starting thinking_os MCP server (stdio)...")
|
|
3149
|
+
gating = apply_module_tool_gating(mcp)
|
|
3150
|
+
if gating["removed"]:
|
|
3151
|
+
logger.info(
|
|
3152
|
+
"Module gating: removed %d tool(s) for disabled module(s) %s",
|
|
3153
|
+
len(gating["removed"]),
|
|
3154
|
+
gating["disabled_modules"],
|
|
3155
|
+
)
|
|
3156
|
+
mcp.run(transport="stdio")
|
|
3157
|
+
|
|
3158
|
+
|
|
3159
|
+
if __name__ == "__main__":
|
|
3160
|
+
main()
|