diff --git a/docs/agent-integration-reference.md b/docs/agent-integration-reference.md index f8a857331c..04497e9d49 100644 --- a/docs/agent-integration-reference.md +++ b/docs/agent-integration-reference.md @@ -740,6 +740,11 @@ Prompts: `cost_of`. - `sessions_touching_file` — workflow `file-touch`; required capability `read`; mutation authority `none`; owner `polylogue-t46.8.2`. - `cost_of` — workflow `cost-analysis`; required capability `read`; mutation authority `none`; owner `polylogue-t46.8.2`. - `agent_coordination_brief` — workflow `coordination`; required capability `read`; mutation authority `none`; owner `polylogue-t46.8.3`. +- `analyze_errors` — workflow `error-analysis`; required capability `read`; mutation authority `none`; owner `polylogue-il50`. +- `summarize_week` — workflow `weekly-summary`; required capability `read`; mutation authority `none`; owner `polylogue-il50`. +- `extract_code` — workflow `code-extraction`; required capability `read`; mutation authority `none`; owner `polylogue-il50`. +- `compare_sessions` — workflow `session-comparison`; required capability `read`; mutation authority `none`; owner `polylogue-il50`. +- `extract_patterns` — workflow `pattern-extraction`; required capability `read`; mutation authority `none`; owner `polylogue-il50`. ## Source origins diff --git a/docs/generated/mcp-equivalence.json b/docs/generated/mcp-equivalence.json index 7ae868b91e..b26a3bb81f 100644 --- a/docs/generated/mcp-equivalence.json +++ b/docs/generated/mcp-equivalence.json @@ -270,6 +270,41 @@ "name": "agent_coordination_brief", "required_capability": null, "workflow": "coordination" + }, + { + "migration_owner": "polylogue-il50", + "mutation_authority": "none", + "name": "analyze_errors", + "required_capability": null, + "workflow": "error-analysis" + }, + { + "migration_owner": "polylogue-il50", + "mutation_authority": "none", + "name": "summarize_week", + "required_capability": null, + "workflow": "weekly-summary" + }, + { + "migration_owner": "polylogue-il50", + "mutation_authority": "none", + "name": "extract_code", + "required_capability": null, + "workflow": "code-extraction" + }, + { + "migration_owner": "polylogue-il50", + "mutation_authority": "none", + "name": "compare_sessions", + "required_capability": null, + "workflow": "session-comparison" + }, + { + "migration_owner": "polylogue-il50", + "mutation_authority": "none", + "name": "extract_patterns", + "required_capability": null, + "workflow": "pattern-extraction" } ], "resources": [ diff --git a/docs/plans/topology-target.yaml b/docs/plans/topology-target.yaml index d99b5884ae..5d8ed07040 100644 --- a/docs/plans/topology-target.yaml +++ b/docs/plans/topology-target.yaml @@ -37,7 +37,7 @@ files: target: polylogue/agent_integration/manifest.py owner: stable - path: polylogue/agent_integration/spec.py - loc: 945 + loc: 946 target: polylogue/agent_integration/spec.py owner: stable - path: polylogue/annotations/__init__.py @@ -70,7 +70,7 @@ files: owner: stable cross_cut: { api: async } - path: polylogue/api/archive.py - loc: 6820 + loc: 7122 target: polylogue/api/archive.py owner: stable cross_cut: { api: async } @@ -179,7 +179,7 @@ files: owner: archive-artifact-taxonomy reason: archive-domain semantics - path: polylogue/archive/artifact_taxonomy/runtime.py - loc: 446 + loc: 477 target: polylogue/archive/artifact_taxonomy/runtime.py owner: archive-artifact-taxonomy reason: archive-domain semantics @@ -221,7 +221,7 @@ files: owner: archive-filter reason: archive-domain filter semantics - path: polylogue/archive/filter/filters.py - loc: 167 + loc: 179 target: polylogue/archive/filter/filters.py owner: archive-filter reason: archive-domain filter semantics @@ -330,12 +330,12 @@ files: owner: archive-query reason: archive-domain query semantics - path: polylogue/archive/query/archive_execution.py - loc: 703 + loc: 713 target: polylogue/archive/query/archive_execution.py owner: archive-query reason: archive-domain query semantics - path: polylogue/archive/query/attached_units.py - loc: 219 + loc: 282 target: polylogue/archive/query/attached_units.py owner: archive-query reason: archive-domain query semantics @@ -360,7 +360,7 @@ files: owner: archive-query reason: archive-domain query semantics - path: polylogue/archive/query/expression.py - loc: 3567 + loc: 3865 target: polylogue/archive/query/expression.py owner: archive-query reason: archive-domain query semantics @@ -370,12 +370,12 @@ files: owner: archive-query reason: archive-domain query semantics - path: polylogue/archive/query/fields.py - loc: 988 + loc: 991 target: polylogue/archive/query/fields.py owner: archive-query reason: archive-domain query semantics - path: polylogue/archive/query/metadata.py - loc: 1439 + loc: 1495 target: polylogue/archive/query/metadata.py owner: archive-query reason: archive-domain query semantics @@ -465,7 +465,7 @@ files: owner: archive-query reason: archive-domain query semantics - path: polylogue/archive/query/search_hits.py - loc: 340 + loc: 369 target: polylogue/archive/query/search_hits.py owner: archive-query reason: archive-domain query semantics @@ -485,7 +485,7 @@ files: owner: archive-query reason: archive-domain query semantics - path: polylogue/archive/query/spec.py - loc: 611 + loc: 648 target: polylogue/archive/query/spec.py owner: archive-query reason: archive-domain query semantics @@ -500,7 +500,7 @@ files: owner: archive-query reason: archive-domain query semantics - path: polylogue/archive/query/unit_results.py - loc: 582 + loc: 723 target: polylogue/archive/query/unit_results.py owner: archive-query reason: archive-domain query semantics @@ -627,7 +627,7 @@ files: owner: archive-session reason: archive-domain semantics - path: polylogue/archive/session/documents.py - loc: 147 + loc: 150 target: polylogue/archive/session/documents.py owner: archive-session reason: archive-domain semantics @@ -654,7 +654,7 @@ files: owner: archive-session reason: archive-domain semantics - path: polylogue/archive/session/models.py - loc: 353 + loc: 371 target: polylogue/archive/session/models.py owner: archive-session reason: archive-domain semantics @@ -674,7 +674,7 @@ files: owner: archive-session reason: archive-domain semantics - path: polylogue/archive/session/runtime.py - loc: 632 + loc: 652 target: polylogue/archive/session/runtime.py owner: archive-session reason: archive-domain semantics @@ -819,19 +819,19 @@ files: target: polylogue/cli/__main__.py owner: stable - path: polylogue/cli/archive_query.py - loc: 2775 + loc: 2784 target: polylogue/cli/archive_query.py owner: stable - path: polylogue/cli/click_app.py - loc: 651 + loc: 652 target: polylogue/cli/click_app.py owner: stable - path: polylogue/cli/click_command_registration.py - loc: 213 + loc: 215 target: polylogue/cli/click_command_registration.py owner: stable - path: polylogue/cli/click_option_groups.py - loc: 390 + loc: 399 target: polylogue/cli/click_option_groups.py owner: stable - path: polylogue/cli/command_inventory.py @@ -866,6 +866,10 @@ files: loc: 116 target: polylogue/cli/commands/check.py owner: stable + - path: polylogue/cli/commands/compare.py + loc: 207 + target: polylogue/cli/commands/compare.py + owner: stable - path: polylogue/cli/commands/completions.py loc: 107 target: polylogue/cli/commands/completions.py @@ -1031,7 +1035,7 @@ files: target: polylogue/cli/commands/scan_secrets.py owner: stable - path: polylogue/cli/commands/status.py - loc: 2467 + loc: 2482 target: polylogue/cli/commands/status.py owner: stable - path: polylogue/cli/commands/status_diagnostics.py @@ -1175,7 +1179,7 @@ files: target: polylogue/cli/root_request.py owner: stable - path: polylogue/cli/select.py - loc: 272 + loc: 262 target: polylogue/cli/select.py owner: stable - path: polylogue/cli/shared/check_maintenance.py @@ -1371,7 +1375,7 @@ files: owner: core-primitive reason: core primitive - path: polylogue/core/enums.py - loc: 613 + loc: 619 target: polylogue/core/enums.py owner: core-primitive reason: core primitive @@ -1431,7 +1435,7 @@ files: owner: core-primitive reason: core primitive - path: polylogue/core/provider_identity.py - loc: 177 + loc: 179 target: polylogue/core/provider_identity.py owner: core-primitive reason: core primitive @@ -1451,7 +1455,7 @@ files: owner: core-primitive reason: core primitive - path: polylogue/core/sources.py - loc: 438 + loc: 447 target: polylogue/core/sources.py owner: core-primitive reason: core primitive @@ -1535,7 +1539,7 @@ files: target: polylogue/daemon/catchup_status.py owner: stable - path: polylogue/daemon/cli.py - loc: 2869 + loc: 2934 target: polylogue/daemon/cli.py owner: stable - path: polylogue/daemon/compare.py @@ -1555,7 +1559,7 @@ files: target: polylogue/daemon/convergence_debt_status.py owner: stable - path: polylogue/daemon/convergence_stages.py - loc: 2040 + loc: 2086 target: polylogue/daemon/convergence_stages.py owner: stable - path: polylogue/daemon/convergence_standing_queries.py @@ -1856,7 +1860,7 @@ files: target: polylogue/demo/workspace.py owner: stable - path: polylogue/hooks/__init__.py - loc: 917 + loc: 987 target: polylogue/hooks/__init__.py owner: stable - path: polylogue/insights/__init__.py @@ -2024,6 +2028,10 @@ files: loc: 143 target: polylogue/insights/measurement/ratio.py owner: stable + - path: polylogue/insights/measurement/registered_metrics.py + loc: 57 + target: polylogue/insights/measurement/registered_metrics.py + owner: stable - path: polylogue/insights/measurement/registration.py loc: 107 target: polylogue/insights/measurement/registration.py @@ -2085,7 +2093,7 @@ files: target: polylogue/insights/session_analytics.py owner: stable - path: polylogue/insights/session_commit.py - loc: 917 + loc: 966 target: polylogue/insights/session_commit.py owner: stable - path: polylogue/insights/session_label.py @@ -2282,7 +2290,7 @@ files: target: polylogue/mcp/declarations/models.py owner: stable - path: polylogue/mcp/declarations/registry.py - loc: 544 + loc: 552 target: polylogue/mcp/declarations/registry.py owner: stable - path: polylogue/mcp/insight_tool_contracts.py @@ -2294,7 +2302,7 @@ files: target: polylogue/mcp/mutation_support.py owner: stable - path: polylogue/mcp/payloads.py - loc: 1065 + loc: 1113 target: polylogue/mcp/payloads.py owner: stable - path: polylogue/mcp/query_contracts.py @@ -2306,15 +2314,15 @@ files: target: polylogue/mcp/server.py owner: stable - path: polylogue/mcp/server_cutover.py - loc: 2049 + loc: 2345 target: polylogue/mcp/server_cutover.py owner: stable - path: polylogue/mcp/server_prompts.py - loc: 563 + loc: 562 target: polylogue/mcp/server_prompts.py owner: stable - path: polylogue/mcp/server_resources.py - loc: 420 + loc: 438 target: polylogue/mcp/server_resources.py owner: stable - path: polylogue/mcp/server_support.py @@ -2560,7 +2568,7 @@ files: target: polylogue/product/workflows.py owner: stable - path: polylogue/readiness/__init__.py - loc: 1013 + loc: 1049 target: polylogue/readiness/__init__.py owner: stable - path: polylogue/readiness/capability.py @@ -2593,7 +2601,7 @@ files: target: polylogue/rendering/core_messages.py owner: stable - path: polylogue/rendering/formatting.py - loc: 240 + loc: 242 target: polylogue/rendering/formatting.py owner: stable - path: polylogue/rendering/renderers/__init__.py @@ -2630,7 +2638,7 @@ files: target: polylogue/rendering/semantic_card_placement.py owner: stable - path: polylogue/rendering/semantic_card_registry.py - loc: 549 + loc: 550 target: polylogue/rendering/semantic_card_registry.py owner: stable - path: polylogue/rendering/semantic_cards.py @@ -3167,7 +3175,7 @@ files: target: polylogue/sources/__init__.py owner: stable - path: polylogue/sources/assembly.py - loc: 116 + loc: 112 target: polylogue/sources/assembly.py owner: stable - path: polylogue/sources/assembly_chatgpt.py @@ -3175,7 +3183,7 @@ files: target: polylogue/sources/assembly_chatgpt.py owner: stable - path: polylogue/sources/assembly_claude_code.py - loc: 228 + loc: 203 target: polylogue/sources/assembly_claude_code.py owner: stable - path: polylogue/sources/assembly_codex.py @@ -3203,7 +3211,7 @@ files: target: polylogue/sources/decoders.py owner: stable - path: polylogue/sources/dispatch.py - loc: 1400 + loc: 1430 target: polylogue/sources/dispatch.py owner: stable - path: polylogue/sources/drive/__init__.py @@ -3255,7 +3263,7 @@ files: target: polylogue/sources/emitter.py owner: stable - path: polylogue/sources/hooks.py - loc: 338 + loc: 499 target: polylogue/sources/hooks.py owner: stable - path: polylogue/sources/import_explain.py @@ -3275,11 +3283,11 @@ files: target: polylogue/sources/live/_lag_sample_ddl.py owner: stable - path: polylogue/sources/live/append_ingest.py - loc: 247 + loc: 283 target: polylogue/sources/live/append_ingest.py owner: stable - path: polylogue/sources/live/batch.py - loc: 3081 + loc: 3134 target: polylogue/sources/live/batch.py owner: stable - path: polylogue/sources/live/batch_observability.py @@ -3343,7 +3351,7 @@ files: target: polylogue/sources/live/watcher.py owner: stable - path: polylogue/sources/origin_specs.py - loc: 1302 + loc: 1364 target: polylogue/sources/origin_specs.py owner: stable - path: polylogue/sources/parsers/antigravity.py @@ -3360,7 +3368,7 @@ files: owner: stable cross_cut: { lifecycle: model } - path: polylogue/sources/parsers/base_support.py - loc: 278 + loc: 313 target: polylogue/sources/parsers/base_support.py owner: stable - path: polylogue/sources/parsers/beads.py @@ -3372,7 +3380,7 @@ files: target: polylogue/sources/parsers/browser_capture.py owner: stable - path: polylogue/sources/parsers/chatgpt.py - loc: 1139 + loc: 1317 target: polylogue/sources/parsers/chatgpt.py owner: stable - path: polylogue/sources/parsers/chatgpt_codex_sidecar.py @@ -3384,11 +3392,11 @@ files: target: polylogue/sources/parsers/chatgpt_sidecars.py owner: stable - path: polylogue/sources/parsers/claude/__init__.py - loc: 64 + loc: 78 target: polylogue/sources/parsers/claude/__init__.py owner: stable - path: polylogue/sources/parsers/claude/ai_parser.py - loc: 318 + loc: 672 target: polylogue/sources/parsers/claude/ai_parser.py owner: stable - path: polylogue/sources/parsers/claude/code_detection.py @@ -3396,7 +3404,7 @@ files: target: polylogue/sources/parsers/claude/code_detection.py owner: stable - path: polylogue/sources/parsers/claude/code_parser.py - loc: 1694 + loc: 2019 target: polylogue/sources/parsers/claude/code_parser.py owner: stable - path: polylogue/sources/parsers/claude/common.py @@ -3412,7 +3420,7 @@ files: target: polylogue/sources/parsers/claude/index.py owner: stable - path: polylogue/sources/parsers/claude/orchestration.py - loc: 280 + loc: 230 target: polylogue/sources/parsers/claude/orchestration.py owner: stable - path: polylogue/sources/parsers/codex.py @@ -3468,7 +3476,7 @@ files: target: polylogue/sources/parsers/hermes_verification.py owner: stable - path: polylogue/sources/parsers/local_agent.py - loc: 653 + loc: 677 target: polylogue/sources/parsers/local_agent.py owner: stable - path: polylogue/sources/provider_completeness.py @@ -3524,7 +3532,7 @@ files: owner: stable cross_cut: { lifecycle: model } - path: polylogue/sources/revision_backfill.py - loc: 1821 + loc: 1822 target: polylogue/sources/revision_backfill.py owner: stable - path: polylogue/sources/source_acquisition.py @@ -3565,7 +3573,7 @@ files: target: TBD owner: storage-domain - path: polylogue/storage/archive_readiness.py - loc: 1244 + loc: 1294 target: TBD owner: storage-domain - path: polylogue/storage/archive_views.py @@ -3772,7 +3780,7 @@ files: target: polylogue/storage/insights/session/latency_profiles.py owner: stable - path: polylogue/storage/insights/session/profiles.py - loc: 836 + loc: 844 target: polylogue/storage/insights/session/profiles.py owner: stable - path: polylogue/storage/insights/session/rebuild.py @@ -3780,7 +3788,7 @@ files: target: polylogue/storage/insights/session/rebuild.py owner: stable - path: polylogue/storage/insights/session/records.py - loc: 173 + loc: 180 target: polylogue/storage/insights/session/records.py owner: stable - path: polylogue/storage/insights/session/refresh.py @@ -3808,7 +3816,7 @@ files: target: polylogue/storage/insights/session/status.py owner: stable - path: polylogue/storage/insights/session/storage.py - loc: 833 + loc: 841 target: polylogue/storage/insights/session/storage.py owner: stable - path: polylogue/storage/insights/session/threads.py @@ -4096,7 +4104,7 @@ files: target: polylogue/storage/sqlite/archive_tiers/__init__.py owner: stable - path: polylogue/storage/sqlite/archive_tiers/archive.py - loc: 11324 + loc: 11382 target: polylogue/storage/sqlite/archive_tiers/archive.py owner: stable - path: polylogue/storage/sqlite/archive_tiers/archive_init.py @@ -4136,7 +4144,7 @@ files: target: polylogue/storage/sqlite/archive_tiers/embeddings.py owner: stable - path: polylogue/storage/sqlite/archive_tiers/index.py - loc: 1989 + loc: 2009 target: polylogue/storage/sqlite/archive_tiers/index.py owner: stable - path: polylogue/storage/sqlite/archive_tiers/index_convergence.py @@ -4208,11 +4216,11 @@ files: target: polylogue/storage/sqlite/archive_tiers/user_overlay.py owner: stable - path: polylogue/storage/sqlite/archive_tiers/user_write.py - loc: 2539 + loc: 2608 target: polylogue/storage/sqlite/archive_tiers/user_write.py owner: stable - path: polylogue/storage/sqlite/archive_tiers/write.py - loc: 5678 + loc: 6025 target: polylogue/storage/sqlite/archive_tiers/write.py owner: stable - path: polylogue/storage/sqlite/async_sqlite.py @@ -4244,7 +4252,7 @@ files: target: polylogue/storage/sqlite/finding_provenance.py owner: stable - path: polylogue/storage/sqlite/lifecycle.py - loc: 602 + loc: 622 target: polylogue/storage/sqlite/lifecycle.py owner: stable - path: polylogue/storage/sqlite/maintenance.py @@ -4544,7 +4552,7 @@ files: target: polylogue/surfaces/chronicle.py owner: stable - path: polylogue/surfaces/payloads.py - loc: 3887 + loc: 3911 target: polylogue/surfaces/payloads.py owner: stable - path: polylogue/surfaces/projection_spec.py @@ -4588,7 +4596,7 @@ files: owner: stable cross_cut: { api: async } - path: polylogue/ui/theme.py - loc: 532 + loc: 534 target: polylogue/ui/theme.py owner: stable - path: polylogue/ui/tui/__init__.py diff --git a/polylogue/agent_integration/data/deep-reference.md b/polylogue/agent_integration/data/deep-reference.md index f8a857331c..04497e9d49 100644 --- a/polylogue/agent_integration/data/deep-reference.md +++ b/polylogue/agent_integration/data/deep-reference.md @@ -740,6 +740,11 @@ Prompts: `cost_of`. - `sessions_touching_file` — workflow `file-touch`; required capability `read`; mutation authority `none`; owner `polylogue-t46.8.2`. - `cost_of` — workflow `cost-analysis`; required capability `read`; mutation authority `none`; owner `polylogue-t46.8.2`. - `agent_coordination_brief` — workflow `coordination`; required capability `read`; mutation authority `none`; owner `polylogue-t46.8.3`. +- `analyze_errors` — workflow `error-analysis`; required capability `read`; mutation authority `none`; owner `polylogue-il50`. +- `summarize_week` — workflow `weekly-summary`; required capability `read`; mutation authority `none`; owner `polylogue-il50`. +- `extract_code` — workflow `code-extraction`; required capability `read`; mutation authority `none`; owner `polylogue-il50`. +- `compare_sessions` — workflow `session-comparison`; required capability `read`; mutation authority `none`; owner `polylogue-il50`. +- `extract_patterns` — workflow `pattern-extraction`; required capability `read`; mutation authority `none`; owner `polylogue-il50`. ## Source origins diff --git a/polylogue/agent_integration/data/integration-manifest.json b/polylogue/agent_integration/data/integration-manifest.json index dc1393449b..a92cadae9a 100644 --- a/polylogue/agent_integration/data/integration-manifest.json +++ b/polylogue/agent_integration/data/integration-manifest.json @@ -46,7 +46,12 @@ "unacknowledged_failures", "sessions_touching_file", "cost_of", - "agent_coordination_brief" + "agent_coordination_brief", + "analyze_errors", + "summarize_week", + "extract_code", + "compare_sessions", + "extract_patterns" ], "recipe_ids": [ "resume-session", diff --git a/polylogue/agent_integration/data/integration-spec.json b/polylogue/agent_integration/data/integration-spec.json index f8a26de844..f460f233ac 100644 --- a/polylogue/agent_integration/data/integration-spec.json +++ b/polylogue/agent_integration/data/integration-spec.json @@ -215,6 +215,41 @@ "name": "agent_coordination_brief", "required_capability": null, "workflow": "coordination" + }, + { + "migration_owner": "polylogue-il50", + "mutation_authority": "none", + "name": "analyze_errors", + "required_capability": null, + "workflow": "error-analysis" + }, + { + "migration_owner": "polylogue-il50", + "mutation_authority": "none", + "name": "summarize_week", + "required_capability": null, + "workflow": "weekly-summary" + }, + { + "migration_owner": "polylogue-il50", + "mutation_authority": "none", + "name": "extract_code", + "required_capability": null, + "workflow": "code-extraction" + }, + { + "migration_owner": "polylogue-il50", + "mutation_authority": "none", + "name": "compare_sessions", + "required_capability": null, + "workflow": "session-comparison" + }, + { + "migration_owner": "polylogue-il50", + "mutation_authority": "none", + "name": "extract_patterns", + "required_capability": null, + "workflow": "pattern-extraction" } ], "target_resources": [ diff --git a/polylogue/mcp/declarations/registry.py b/polylogue/mcp/declarations/registry.py index 117c57d411..7d587f74f9 100644 --- a/polylogue/mcp/declarations/registry.py +++ b/polylogue/mcp/declarations/registry.py @@ -525,6 +525,14 @@ def declared_tool_names(capabilities: MCPCapabilities = _ALL_CAPABILITIES_ENABLE MCPPromptDeclaration("sessions_touching_file", "file-touch", None, "none", "polylogue-t46.8.2"), MCPPromptDeclaration("cost_of", "cost-analysis", None, "none", "polylogue-t46.8.2"), MCPPromptDeclaration("agent_coordination_brief", "coordination", None, "none", "polylogue-t46.8.3"), + # Live-registered (polylogue/mcp/server_prompts.py) but previously absent + # here, leaving completeness/discovery consumers blind to them + # (polylogue-il50). + MCPPromptDeclaration("analyze_errors", "error-analysis", None, "none", "polylogue-il50"), + MCPPromptDeclaration("summarize_week", "weekly-summary", None, "none", "polylogue-il50"), + MCPPromptDeclaration("extract_code", "code-extraction", None, "none", "polylogue-il50"), + MCPPromptDeclaration("compare_sessions", "session-comparison", None, "none", "polylogue-il50"), + MCPPromptDeclaration("extract_patterns", "pattern-extraction", None, "none", "polylogue-il50"), ) if len(TARGET_DEFAULT_READ_ALGEBRA) > 15: diff --git a/polylogue/mcp/server_cutover.py b/polylogue/mcp/server_cutover.py index e9526f4b29..61aa5789d5 100644 --- a/polylogue/mcp/server_cutover.py +++ b/polylogue/mcp/server_cutover.py @@ -691,6 +691,15 @@ async def query( ) -> str: """Execute a terminal DSL page, or resume it using only its q2 token. + The default projection (unit-source rows) honours + origin/tag/repo/since/until/min_messages/max_messages/min_words as + additional session-scope filters applied on top of ``expression``. + ``sort`` has no meaning for unit-source rows (there is no session + ordering to apply) and raises ``invalid_argument`` if given with the + default projection; use ``projection="sessions"`` for sorted session + listings. ``origin`` (all projections) is validated against the + known origin vocabulary and rejected loudly if unrecognised. + ``projection="sessions"`` switches to session-level rows instead of unit-source rows: ``expression`` becomes a free-text ranked search (top-k) when given, or an exhaustive listing filtered by @@ -720,6 +729,26 @@ async def run() -> str: if reference_result is not None: return reference_result + if origin is not None: + from polylogue.core.sources import CORE_SCHEMA_ORIGINS + + bad_origins = [token.strip() for token in origin.split(",") if token.strip()] + bad_origins = [token for token in bad_origins if token not in CORE_SCHEMA_ORIGINS] + if bad_origins: + return hooks.error_json( + f"unknown origin(s): {', '.join(bad_origins)}. Valid: {', '.join(CORE_SCHEMA_ORIGINS)}", + code="invalid_argument", + tool="query", + ) + + if projection == "default" and sort is not None: + return hooks.error_json( + "query(projection='default') does not support sort; " + "use projection='sessions' for sorted session listings", + code="invalid_argument", + tool="query", + ) + if projection == "sessions": if continuation is not None: return hooks.error_json( @@ -781,6 +810,14 @@ async def run() -> str: expression, limit=limit, continuation=continuation, + origin=origin, + tag=tag, + repo=repo, + since=since, + until=until, + min_messages=min_messages, + max_messages=max_messages, + min_words=min_words, ) return hooks.json_payload(payload) except QueryContinuationInvalidError as exc: diff --git a/polylogue/mcp/server_prompts.py b/polylogue/mcp/server_prompts.py index b7f1a6e0c7..362b78fd11 100644 --- a/polylogue/mcp/server_prompts.py +++ b/polylogue/mcp/server_prompts.py @@ -460,10 +460,9 @@ async def resume_context(repo: str = "", limit: int = 5) -> str: return f"""Rebuild working context for repo '{repo_name}' from the Polylogue archive. Call sequence: -1. find_resume_candidates(repo_path="{cwd}", cwd=, recent_files=, limit={limit}) — ranked resumable logical sessions for this checkout. -2. get_resume_brief(session_id=, repo_path="{cwd}", recent_files=) — typed brief: goals, open threads, next actions, provenance refs, and overlap_basis. -3. agent_coordination_brief(view="self") — check concurrent agents/worktrees before claiming work. -4. blackboard_list(scope_repo="{repo_name}", unresolved=True) — unresolved notes/handoffs addressed to agents here. +1. context(intent="resume", repo_path="{cwd}", cwd="{cwd}", recent_files=, limit={limit}) — SessionStart preamble in one call: session lineage, ranked resume candidates, project git state, and provenance-gated assertion guidance. +2. status(scope="coordination") — check concurrent agents/worktrees before claiming work. +3. query(projection="blackboard", limit=20) — blackboard notes (unfiltered listing; filter client-side on scope_repo="{repo_name}" for notes addressed to agents here). Rules: - Cite refs (session_id, message_id) instead of pasting transcripts; fetch full text only for messages you will act on. @@ -473,14 +472,14 @@ async def resume_context(repo: str = "", limit: int = 5) -> str: @mcp.prompt() async def postmortem_last(repo: str = "", since: str = "14d") -> str: """Postmortem the most recent failed or abandoned session for a repo.""" - repo_name, cwd = _repo_context(repo) + repo_name, _cwd = _repo_context(repo) return f"""Postmortem the most recent failed or abandoned session for repo '{repo_name}'. Call sequence: -1. find_abandoned_sessions(repo_path="{cwd}", since="{since}") and find_stuck_sessions(since="{since}") — candidates with dangling work or stuck tool calls. -2. Pick the most recent relevant session; orient with get_session_summary(id=). -3. get_postmortem_bundle(repo="{repo_name}", since="{since}") — forensic bundle: timeline, decisions, tool errors. -4. get_pathologies(repo="{repo_name}", since="{since}") — detected anti-patterns in the same window. +1. query(projection="abandoned_sessions", repo="{repo_name}", since="{since}") and query(projection="stuck_sessions", repo="{repo_name}", since="{since}") — candidates with dangling work or stuck tool calls. +2. Pick the most recent relevant session; orient with get(ref="session:"). +3. query(projection="postmortem", repo="{repo_name}", since="{since}") — forensic bundle: timeline, decisions, tool errors. +4. query(projection="pathologies", repo="{repo_name}", since="{since}") — detected anti-patterns in the same window. Report: what was attempted, where it failed (cite tool_result errors by ref), what remained undone, and the smallest next action. """ @@ -493,9 +492,8 @@ async def decisions_about(topic: str, limit: int = 20) -> str: return f"""Find what was decided about '{topic}'. Call sequence: -1. list_assertion_claims(kinds="decision,judgment,lesson", statuses="active,candidate", limit={limit}) — recorded decisions (authoritative when user-authored). -2. query(expression={assertion_query!r}) — targeted assertion search. -3. search(query={ranked_query!r}, limit=10) — decision discussions never recorded as assertions. +1. query(expression={assertion_query!r}, limit={limit}) — recorded decision assertions about the topic (authoritative when user-authored). +2. query(projection="sessions", expression={ranked_query!r}, limit=10) — ranked free-text search for decision discussions never recorded as assertions. Rules: - Recorded assertions outrank inferred prose; label each finding as recorded vs inferred. @@ -511,8 +509,8 @@ async def unacknowledged_failures(repo: str = "", since: str = "7d") -> str: Call sequence: 1. query(expression={failure_query!r}, limit=20) — action rows for sessions containing failed tool outcomes. -2. find_stuck_sessions(since="{since}") — sessions whose provider tool calls are bounded as stuck. -3. For each hit: list_marks(session_id=) and list_annotations for that session — an existing mark/annotation means acknowledged. +2. query(projection="stuck_sessions", repo="{repo_name}", since="{since}") — sessions whose provider tool calls are bounded as stuck. +3. query(projection="marks", limit=50) and query(projection="annotations", limit=50) — unfiltered listings; filter client-side by session_id per hit. An existing mark/annotation for that session means acknowledged. Report only sessions with failures and no acknowledgment; cite the failing action refs (tool, path, error). """ @@ -528,8 +526,8 @@ async def sessions_touching_file(path: str, repo: str = "") -> str: Call sequence: 1. query(expression={repository_query!r}, limit=20) — file/action rows for sessions that touched the path. 2. query(expression={path_query!r}, limit=20) — per-file action rows (edits, reads, shell references). -3. search(query='"{path}"', limit=10) — mentions in prose that never became edits. -4. get_session_summary(id=) on each hit for orientation. +3. query(projection="sessions", expression='"{path}"', limit=10) — ranked free-text search for mentions in prose that never became edits. +4. get(ref="session:") on each hit for orientation. Rules: path matching is substring — prefer repo-relative fragments (e.g. polylogue/mcp/server_prompts.py) over bare filenames. """ diff --git a/polylogue/mcp/server_resources.py b/polylogue/mcp/server_resources.py index b3374cc709..484eb23b01 100644 --- a/polylogue/mcp/server_resources.py +++ b/polylogue/mcp/server_resources.py @@ -5,7 +5,7 @@ import json import sqlite3 from dataclasses import asdict -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from polylogue.mcp.archive_support import ( archive_session_list_payload, @@ -29,6 +29,19 @@ from polylogue.mcp.server_support import ServerCallbacks +def _without_migration_owner(entry: Any) -> dict[str, object]: + """``asdict(entry)`` minus the internal ``migration_owner`` bookkeeping + field -- a tracking-bead reference, not agent-facing capability data. + + Used for the ``polylogue://capabilities/query`` discovery payload's + ``mcp_algebra`` roster, which is under a hard MCP response byte budget + (``MCP_RESPONSE_BUDGET_BYTES``); dropping a field that exists only for + the repo's own migration bookkeeping keeps that budget from being + consumed by data the calling agent has no use for. + """ + return {key: value for key, value in asdict(entry).items() if key != "migration_owner"} + + def register_resources(mcp: FastMCP, hooks: ServerCallbacks) -> None: """Register MCP resources on the given server.""" @@ -247,9 +260,14 @@ def query_capabilities_resource() -> str: "coverage": "current index generation; check readiness for freshness", }, "mcp_algebra": { - "read_transactions": [asdict(entry) for entry in TARGET_DEFAULT_READ_ALGEBRA], - "resources": [asdict(entry) for entry in TARGET_RESOURCES], - "prompts": [asdict(entry) for entry in TARGET_PROMPTS], + # migration_owner is internal tracking-bead bookkeeping, + # not agent-facing capability data; dropped from all + # three algebra lists to keep this byte-budgeted + # catalog under MCP_RESPONSE_BUDGET_BYTES as the + # declared surface grows (polylogue-il50). + "read_transactions": [_without_migration_owner(entry) for entry in TARGET_DEFAULT_READ_ALGEBRA], + "resources": [_without_migration_owner(entry) for entry in TARGET_RESOURCES], + "prompts": [_without_migration_owner(entry) for entry in TARGET_PROMPTS], }, "units": units, } diff --git a/tests/infra/mcp.py b/tests/infra/mcp.py index 6acc0119fb..efecfd4edc 100644 --- a/tests/infra/mcp.py +++ b/tests/infra/mcp.py @@ -11,7 +11,7 @@ from polylogue.archive.models import Session from polylogue.core.enums import Provider from polylogue.mcp.declarations.models import MCPCapabilities -from polylogue.mcp.declarations.registry import declared_tool_names +from polylogue.mcp.declarations.registry import TARGET_PROMPTS, declared_tool_names from tests.infra.builders import make_conv, make_msg MCP_TOOL_NAME_BASELINE = frozenset({"query", "read", "get", "explain", "context", "status"}) @@ -27,25 +27,23 @@ # declaration in the same change cannot make the test surface self-authorize. EXPECTED_TOOL_NAMES = set(declared_tool_names(ALL_CAPABILITIES)) -EXPECTED_RESOURCE_URIS = { - "polylogue://agent/manual", - "polylogue://agent/reference", - "polylogue://agent/manifest", - "polylogue://capabilities/query", -} - -EXPECTED_RESOURCE_TEMPLATE_URIS = { - "polylogue://session/{conv_id}", -} - -EXPECTED_PROMPT_NAMES = { - "resume_context", - "postmortem_last", - "decisions_about", - "unacknowledged_failures", - "sessions_touching_file", - "cost_of", -} +# Prompt discovery, like tool discovery above, is declaration-derived rather +# than a hand-copied literal set -- a prompt registered in +# polylogue/mcp/server_prompts.py without a matching TARGET_PROMPTS entry (or +# vice versa) fails discovery tests instead of drifting silently +# (polylogue-il50: the prior hand-maintained set here was never referenced by +# any test and had gone stale in both directions). +EXPECTED_PROMPT_NAMES = {entry.name for entry in TARGET_PROMPTS} + +# NOTE: there is no declaration-derived resource-URI pin yet. TARGET_RESOURCES +# (polylogue/mcp/declarations/registry.py) describes an aspirational future +# resource surface (polylogue-t46.8.2/polylogue-t46.8.3) that does not match +# today's live registrations in polylogue/mcp/server_resources.py, so +# deriving an expected set from it here would assert something not yet true. +# A prior hand-maintained EXPECTED_RESOURCE_URIS/EXPECTED_RESOURCE_TEMPLATE_URIS +# pair was found unreferenced and stale in both directions (polylogue-il50) +# and removed rather than left as misleading dead code; reintroduce it once +# TARGET_RESOURCES is reconciled with live registration. SurfaceResult = TypeVar("SurfaceResult") MCPSurfaceHandler: TypeAlias = Callable[..., str | Awaitable[str]] diff --git a/tests/unit/api/test_facade_contracts.py b/tests/unit/api/test_facade_contracts.py index aaf86d3ef9..270e221650 100644 --- a/tests/unit/api/test_facade_contracts.py +++ b/tests/unit/api/test_facade_contracts.py @@ -752,6 +752,37 @@ def test_archive_facet_buckets_count_unique_sessions_for_duplicate_hits() -> Non assert result.tags == {"work": 1} +async def test_archive_facet_buckets_include_deferred_default_populates_sql_families(tmp_path: Path) -> None: + """``_archive_facet_buckets``'s shipped default (``include_deferred=True``) + must actually run the SQL facet-family aggregation (polylogue-f5tq). + + The only prior test of this helper passed a ``SimpleNamespace`` with + ``_conn=None`` and ``include_deferred=False`` -- it could not exercise the + default path at all: passing ``True`` against that stub would crash + dereferencing a ``None`` connection. This test drives the real default + against a real ``ArchiveStore`` connection and asserts the SQL-derived + families (role_counts / message_types) are populated, not the hard-coded + empty dicts the ``include_deferred=False`` branch returns. + """ + from polylogue.api.archive import _archive_facet_buckets + + db_path = tmp_path / "index.db" + await _seed_two_sessions(db_path) + + with ArchiveStore(tmp_path) as archive: + result = _archive_facet_buckets(archive, None, include_deferred=True) + + assert result.total_sessions == 2 + assert result.total_messages == 3 + # The SQL-aggregated families below are exactly what the + # ``include_deferred=False`` branch hard-codes to ``{}`` -- a passing + # test against that branch cannot tell them apart from a broken default. + assert result.role_counts, "role_counts must be populated by the shipped default, not left empty" + assert result.role_counts.get("user") == 2 + assert result.role_counts.get("assistant") == 1 + assert result.message_types, "message_types must be populated by the shipped default" + + # --------------------------------------------------------------------------- # 5. Happy path on a seeded archive # --------------------------------------------------------------------------- diff --git a/tests/unit/mcp/test_prompt_registry_pinning.py b/tests/unit/mcp/test_prompt_registry_pinning.py new file mode 100644 index 0000000000..cd76be9de9 --- /dev/null +++ b/tests/unit/mcp/test_prompt_registry_pinning.py @@ -0,0 +1,100 @@ +"""Discovery pinning for the MCP prompt surface (polylogue-il50). + +Two independent gaps found by a shipped-but-dead audit: six of the seven +``TARGET_PROMPTS`` declarations instructed callers to invoke tool names +retired at the ten-tool cutover (e.g. ``find_resume_candidates``, +``get_session_summary``, ``search``), and five live-registered prompts +(``analyze_errors``, ``summarize_week``, ``extract_code``, +``compare_sessions``, ``extract_patterns``) were absent from +``TARGET_PROMPTS``, leaving every completeness/discovery consumer that reads +the declaration blind to them. ``EXPECTED_PROMPT_NAMES`` in +``tests/infra/mcp.py`` existed but was never referenced by any test, so +neither gap was caught. + +This module closes both gaps with declaration-derived pins, mirroring how +``EXPECTED_TOOL_NAMES`` is derived rather than hand-copied: + +1. The declared prompt set (``TARGET_PROMPTS`` -> ``EXPECTED_PROMPT_NAMES``) + must equal the live-registered prompt set on the actual server. +2. Every prompt's rendered instruction text must reference only tool names + that exist on the live ten-tool dispatcher surface -- catching a prompt + that regresses back to naming a retired tool, the exact failure shape + this bead found. +""" + +from __future__ import annotations + +import re +from collections.abc import Mapping +from typing import cast + +import pytest + +from tests.infra.mcp import EXPECTED_PROMPT_NAMES, EXPECTED_TOOL_NAMES, MCPServerUnderTest, invoke_surface_async + +#: Minimal arguments so every prompt renders without error. Prompts with no +#: required parameters use their own defaults (empty dict). +_PROMPT_INVOCATION_ARGS: Mapping[str, dict[str, object]] = { + "decisions_about": {"topic": "schema migration"}, + "sessions_touching_file": {"path": "polylogue/mcp/server_prompts.py"}, + "compare_sessions": {"id1": "example-origin:session-a", "id2": "example-origin:session-b"}, +} + +#: A call-like ``name(`` pattern in a prompt's rendered instruction text. +#: Matches the same shape as the parity check in test_prompt_query_parity.py +#: but generalized to every dispatcher tool, not just ``query``. +_CALL_RE = re.compile(r"\b([a-z_][a-z0-9_]*)\(") + + +def _build_server() -> MCPServerUnderTest: + from polylogue.mcp.server import build_server + + return cast(MCPServerUnderTest, build_server()) + + +def test_registered_prompts_match_target_prompts() -> None: + """The live-registered prompt set and the declared set must be identical. + + Fails in either direction: a prompt registered in server_prompts.py + without a TARGET_PROMPTS entry (the analyze_errors/summarize_week/ + extract_code/compare_sessions/extract_patterns gap), or a declared + prompt that was never actually registered. + """ + server = _build_server() + registered = set(server._prompt_manager._prompts) + assert registered == EXPECTED_PROMPT_NAMES, ( + f"registered-but-undeclared: {sorted(registered - EXPECTED_PROMPT_NAMES)}; " + f"declared-but-unregistered: {sorted(EXPECTED_PROMPT_NAMES - registered)}" + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("prompt_name", sorted(EXPECTED_PROMPT_NAMES)) +async def test_prompt_instructions_reference_only_live_tools(prompt_name: str) -> None: + """Every ``name(`` call-like reference in a prompt's rendered text must + name a tool on the live ten-tool dispatcher surface. + + Direct regression guard for the bead: six declared prompts instructed + callers to invoke retired names (find_resume_candidates, + get_resume_brief, agent_coordination_brief, blackboard_list, + find_abandoned_sessions, get_session_summary, get_postmortem_bundle, + get_pathologies, list_assertion_claims, search, find_stuck_sessions, + list_marks, list_annotations, cost_rollups, session_costs, + provider_usage). A prompt that regresses to any of those again fails + this test. + """ + server = _build_server() + prompt = server._prompt_manager._prompts[prompt_name] + kwargs = _PROMPT_INVOCATION_ARGS.get(prompt_name, {}) + rendered = await invoke_surface_async(prompt.fn, **kwargs) + assert isinstance(rendered, str) + + referenced = set(_CALL_RE.findall(rendered)) + # Prompts may reference prose fragments that happen to match the call + # shape (none currently do); only flag names that look like a tool call + # AND are not a live tool name. + unknown = referenced - EXPECTED_TOOL_NAMES + assert not unknown, ( + f"{prompt_name} references non-tool or retired-tool call-like names {sorted(unknown)}; " + f"live tools are {sorted(EXPECTED_TOOL_NAMES)}" + ) diff --git a/tests/unit/mcp/test_query_default_projection_filters.py b/tests/unit/mcp/test_query_default_projection_filters.py new file mode 100644 index 0000000000..8cf72b9df8 --- /dev/null +++ b/tests/unit/mcp/test_query_default_projection_filters.py @@ -0,0 +1,144 @@ +"""Regression tests for polylogue-hnl7: ``query()``'s default (unit-source) +projection silently dropped origin/tag/repo/since/until/min_messages/ +max_messages/min_words -- only expression/limit/continuation reached +``query_units``. Live-archive evidence: filtering ``role:user`` messages by +``origin="claude-code-session"`` returned the ALL-origin total, and an +unrecognised origin was accepted silently instead of rejected. + +These tests seed two sessions with different origins and assert the default +projection actually scopes to the requested origin, and that an unknown +origin / an unsupported ``sort`` on the default projection fail loudly +instead of being ignored. +""" + +from __future__ import annotations + +import json +from collections.abc import Awaitable, Callable, Iterator +from contextlib import contextmanager +from pathlib import Path +from typing import cast + +import pytest + +from polylogue.mcp.declarations.models import MCPCapabilities +from tests.infra.mcp import MCPServerUnderTest, invoke_surface_async + + +@contextmanager +def _installed_runtime_services(archive_root: Path) -> Iterator[None]: + """Install real RuntimeServices for ``archive_root``, restoring whatever was active before.""" + from polylogue.config import Config + from polylogue.mcp import server_support + from polylogue.services import RuntimeServices + + services = RuntimeServices( + config=Config(archive_root=archive_root, render_root=archive_root.parent / "render", sources=[]), + ) + try: + original: RuntimeServices | None = server_support._get_runtime_services() + except RuntimeError: + original = None + server_support._set_runtime_services(services) + try: + yield + finally: + server_support._set_runtime_services(original) + + +def _build_tools( + capabilities: MCPCapabilities = MCPCapabilities(), +) -> dict[str, Callable[..., str | Awaitable[str]]]: + from polylogue.mcp.server import build_server + + server = cast(MCPServerUnderTest, build_server(capabilities=capabilities)) + return {name: tool.fn for name, tool in server._tool_manager._tools.items()} + + +def _seed_two_origin_sessions(archive_root: Path) -> None: + """Write one claude-code-session and one chatgpt-export session, each + with a single ``role:user`` message, so an origin-scoped count can be + told apart from the archive-wide count.""" + from polylogue.archive.message.roles import Role + from polylogue.core.enums import BlockType, Provider + from polylogue.sources.parsers.base import ParsedContentBlock, ParsedMessage, ParsedSession + from polylogue.storage.sqlite.archive_tiers.archive import ArchiveStore + + with ArchiveStore(archive_root) as archive: + for provider, native_id in ((Provider.CLAUDE_CODE, "cc-1"), (Provider.CHATGPT, "gpt-1")): + archive.write_parsed( + ParsedSession( + source_name=provider, + provider_session_id=native_id, + title=f"origin filter probe ({provider.value})", + messages=[ + ParsedMessage( + provider_message_id="m1", + role=Role.USER, + text="origin filter probe message", + blocks=[ParsedContentBlock(type=BlockType.TEXT, text="origin filter probe message")], + ) + ], + ) + ) + + +class TestDefaultProjectionFilters: + @pytest.mark.asyncio + async def test_origin_filter_scopes_default_projection(self, tmp_path: Path) -> None: + archive_root = tmp_path / "archive" + _seed_two_origin_sessions(archive_root) + query_fn = _build_tools()["query"] + + with _installed_runtime_services(archive_root): + unfiltered = json.loads(await invoke_surface_async(query_fn, expression="messages where role:user | count")) + scoped = json.loads( + await invoke_surface_async( + query_fn, + expression="messages where role:user | count", + origin="claude-code-session", + ) + ) + + assert unfiltered["items"][0]["count"] == 2 + # Before the fix, ``origin`` never reached ``query_units`` for the + # default projection, so this would also read 2 (the whole-archive + # total) instead of the origin-scoped 1. + assert scoped["items"][0]["count"] == 1 + + @pytest.mark.asyncio + async def test_unknown_origin_rejected_loudly(self, tmp_path: Path) -> None: + archive_root = tmp_path / "archive" + _seed_two_origin_sessions(archive_root) + query_fn = _build_tools()["query"] + + with _installed_runtime_services(archive_root): + result = json.loads( + await invoke_surface_async( + query_fn, + expression="messages where role:user | count", + origin="bogus-origin", + ) + ) + + assert result.get("is_error") is True + assert result.get("code") == "invalid_argument" + assert "bogus-origin" in result.get("message", "") + + @pytest.mark.asyncio + async def test_sort_rejected_on_default_projection(self, tmp_path: Path) -> None: + archive_root = tmp_path / "archive" + _seed_two_origin_sessions(archive_root) + query_fn = _build_tools()["query"] + + with _installed_runtime_services(archive_root): + result = json.loads( + await invoke_surface_async( + query_fn, + expression="messages where role:user | count", + sort="recency", + ) + ) + + assert result.get("is_error") is True + assert result.get("code") == "invalid_argument"