squawkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- squawkit/__init__.py +3 -0
- squawkit/__main__.py +5 -0
- squawkit/db.py +17 -0
- squawkit/defaults.py +225 -0
- squawkit/formatting.py +89 -0
- squawkit/prompts.py +236 -0
- squawkit/server.py +493 -0
- squawkit/session.py +165 -0
- squawkit/workflows.py +407 -0
- squawkit-0.1.0.dist-info/METADATA +49 -0
- squawkit-0.1.0.dist-info/RECORD +13 -0
- squawkit-0.1.0.dist-info/WHEEL +4 -0
- squawkit-0.1.0.dist-info/entry_points.txt +2 -0
squawkit/__init__.py
ADDED
squawkit/__main__.py
ADDED
squawkit/db.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""DuckDB connection for squawkit.
|
|
2
|
+
|
|
3
|
+
Thin wrapper over pluckit's Plucker that returns a fledgling-enabled
|
|
4
|
+
connection proxy with auto-generated macro wrappers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pluckit import Plucker
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_connection(**kwargs):
|
|
11
|
+
"""Create a fledgling-enabled DuckDB connection via pluckit.
|
|
12
|
+
|
|
13
|
+
Accepts the same kwargs as :class:`pluckit.Plucker` (``repo``,
|
|
14
|
+
``profile``, ``modules``, ``init``). Returns the fledgling Connection
|
|
15
|
+
proxy — the same object squawkit's server.py uses as ``con``.
|
|
16
|
+
"""
|
|
17
|
+
return Plucker(**kwargs).connection
|
squawkit/defaults.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Smart project-aware defaults for fledgling tools.
|
|
2
|
+
|
|
3
|
+
Infers sensible default patterns (code globs, doc paths, git revisions)
|
|
4
|
+
from the project at server startup. Users can override via
|
|
5
|
+
.fledgling-python/config.toml. Explicit tool parameters always win.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import subprocess
|
|
12
|
+
import tomllib
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from duckdb import DuckDBPyConnection as Connection
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ProjectDefaults:
|
|
25
|
+
"""Inferred at server startup, cached for the session."""
|
|
26
|
+
|
|
27
|
+
code_pattern: str = "**/*"
|
|
28
|
+
doc_pattern: str = "**/*.md"
|
|
29
|
+
main_branch: str = "main"
|
|
30
|
+
from_rev: str = "HEAD~1"
|
|
31
|
+
to_rev: str = "HEAD"
|
|
32
|
+
languages: list[str] = field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
def scoped_code_pattern(self, path: str | Path) -> str:
|
|
35
|
+
"""Scope the code pattern to a subdirectory path."""
|
|
36
|
+
filename_glob = self.code_pattern.rsplit("/", 1)[-1]
|
|
37
|
+
return str(Path(path) / "**" / filename_glob)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def apply_defaults(
|
|
41
|
+
defaults: ProjectDefaults,
|
|
42
|
+
tool_name: str,
|
|
43
|
+
kwargs: dict[str, object],
|
|
44
|
+
) -> dict[str, object]:
|
|
45
|
+
"""Substitute None params with smart defaults for a given tool.
|
|
46
|
+
|
|
47
|
+
Returns a new dict — does not mutate the input.
|
|
48
|
+
"""
|
|
49
|
+
mapping = TOOL_DEFAULTS.get(tool_name)
|
|
50
|
+
if not mapping:
|
|
51
|
+
return dict(kwargs)
|
|
52
|
+
result = dict(kwargs)
|
|
53
|
+
for param, field_name in mapping.items():
|
|
54
|
+
if result.get(param) is None:
|
|
55
|
+
result[param] = getattr(defaults, field_name)
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_config(root: str | Path) -> dict[str, str]:
|
|
60
|
+
"""Read defaults overrides from .fledgling-python/config.toml.
|
|
61
|
+
|
|
62
|
+
Returns the [defaults] section as a flat dict, or {} if the file
|
|
63
|
+
doesn't exist or has no [defaults] section.
|
|
64
|
+
"""
|
|
65
|
+
config_path = Path(root) / ".fledgling-python" / "config.toml"
|
|
66
|
+
if not config_path.is_file():
|
|
67
|
+
return {}
|
|
68
|
+
with open(config_path, "rb") as f:
|
|
69
|
+
data = tomllib.load(f)
|
|
70
|
+
return dict(data.get("defaults", {}))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Tool name → {param_name: defaults_field_name}
|
|
74
|
+
TOOL_DEFAULTS: dict[str, dict[str, str]] = {
|
|
75
|
+
"find_definitions": {"file_pattern": "code_pattern"},
|
|
76
|
+
"find_in_ast": {"file_pattern": "code_pattern"},
|
|
77
|
+
"code_structure": {"file_pattern": "code_pattern"},
|
|
78
|
+
"complexity_hotspots": {"file_pattern": "code_pattern"},
|
|
79
|
+
"changed_function_summary": {"file_pattern": "code_pattern"},
|
|
80
|
+
"doc_outline": {"file_pattern": "doc_pattern"},
|
|
81
|
+
"file_changes": {"from_rev": "from_rev", "to_rev": "to_rev"},
|
|
82
|
+
"file_diff": {"from_rev": "from_rev", "to_rev": "to_rev"},
|
|
83
|
+
"structural_diff": {"from_rev": "from_rev", "to_rev": "to_rev"},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── Language detection ──────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
# Language name (as returned by project_overview) → file extensions.
|
|
90
|
+
# Hardcoded for now; will be replaced by sitting_duck's extension listing
|
|
91
|
+
# when available.
|
|
92
|
+
LANGUAGE_EXTENSIONS: dict[str, list[str]] = {
|
|
93
|
+
"Python": ["py", "pyi"],
|
|
94
|
+
"JavaScript": ["js", "jsx", "mjs"],
|
|
95
|
+
"TypeScript": ["ts", "tsx"],
|
|
96
|
+
"Rust": ["rs"],
|
|
97
|
+
"Go": ["go"],
|
|
98
|
+
"Java": ["java"],
|
|
99
|
+
"Ruby": ["rb"],
|
|
100
|
+
"C": ["c"],
|
|
101
|
+
"C++": ["cpp", "cc"],
|
|
102
|
+
"C/C++": ["h", "hpp"],
|
|
103
|
+
"SQL": ["sql"],
|
|
104
|
+
"Shell": ["sh", "bash", "zsh"],
|
|
105
|
+
"Kotlin": ["kt", "kts"],
|
|
106
|
+
"Swift": ["swift"],
|
|
107
|
+
"Dart": ["dart"],
|
|
108
|
+
"PHP": ["php"],
|
|
109
|
+
"Lua": ["lua"],
|
|
110
|
+
"Zig": ["zig"],
|
|
111
|
+
"R": ["r", "R"],
|
|
112
|
+
"C#": ["cs"],
|
|
113
|
+
"HCL": ["hcl", "tf"],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Directories to check for docs, in priority order.
|
|
117
|
+
_DOC_DIRS = ["docs", "documentation", "doc", "wiki"]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _code_glob(extensions: list[str]) -> str:
|
|
121
|
+
"""Build a glob pattern from a list of extensions.
|
|
122
|
+
|
|
123
|
+
Uses only the primary (first) extension because DuckDB's glob()
|
|
124
|
+
does not support brace expansion (e.g. ``**/*.{py,pyi}``).
|
|
125
|
+
"""
|
|
126
|
+
return f"**/*.{extensions[0]}"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _find_doc_dir(con: Connection, root: str | Path) -> str | None:
|
|
130
|
+
"""Check for common doc directories under `root` using list_files.
|
|
131
|
+
|
|
132
|
+
Uses an absolute glob (``{root}/{d}/*``) so the probe doesn't depend
|
|
133
|
+
on the connection's ``session_root`` matching the caller's intended
|
|
134
|
+
project root. The previous relative-pattern form silently failed when
|
|
135
|
+
``session_root`` and the ``root`` parameter diverged, causing
|
|
136
|
+
doc_pattern to fall back to ``**/*.md``.
|
|
137
|
+
"""
|
|
138
|
+
root_path = Path(root)
|
|
139
|
+
for d in _DOC_DIRS:
|
|
140
|
+
try:
|
|
141
|
+
pattern = str(root_path / d / "*")
|
|
142
|
+
rows = con.list_files(pattern).fetchall()
|
|
143
|
+
if rows:
|
|
144
|
+
return d
|
|
145
|
+
except Exception:
|
|
146
|
+
log.debug("doc dir probe failed for %s", d, exc_info=True)
|
|
147
|
+
continue
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _infer_main_branch(root: str | Path) -> str:
|
|
152
|
+
"""Detect the default branch from git remote HEAD."""
|
|
153
|
+
try:
|
|
154
|
+
result = subprocess.run(
|
|
155
|
+
["git", "rev-parse", "--abbrev-ref", "origin/HEAD"],
|
|
156
|
+
capture_output=True, text=True, cwd=str(Path(root)), timeout=5,
|
|
157
|
+
)
|
|
158
|
+
if result.returncode == 0:
|
|
159
|
+
# Output is "origin/main" or "origin/master" — strip prefix
|
|
160
|
+
branch = result.stdout.strip().removeprefix("origin/")
|
|
161
|
+
if branch:
|
|
162
|
+
return branch
|
|
163
|
+
except Exception:
|
|
164
|
+
log.debug("git main branch detection failed", exc_info=True)
|
|
165
|
+
return "main"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def infer_defaults(
|
|
169
|
+
con: Connection,
|
|
170
|
+
overrides: dict[str, str] | None = None,
|
|
171
|
+
root: str | Path | None = None,
|
|
172
|
+
) -> ProjectDefaults:
|
|
173
|
+
"""Analyze the project and build smart defaults.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
con: A fledgling Connection to the project.
|
|
177
|
+
overrides: Values from config file that override inference.
|
|
178
|
+
root: Project root for git operations. Defaults to cwd.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
ProjectDefaults with inferred + overridden values.
|
|
182
|
+
"""
|
|
183
|
+
overrides = overrides or {}
|
|
184
|
+
|
|
185
|
+
# ── Code pattern ────────────────────────────────────────────
|
|
186
|
+
code_pattern = "**/*"
|
|
187
|
+
languages: list[str] = []
|
|
188
|
+
try:
|
|
189
|
+
rows = con.project_overview().fetchall()
|
|
190
|
+
# rows are (language, extension, file_count) ordered by count DESC
|
|
191
|
+
if rows:
|
|
192
|
+
# Group by language, sum file counts
|
|
193
|
+
lang_counts: dict[str, int] = {}
|
|
194
|
+
for lang, _ext, count in rows:
|
|
195
|
+
lang_counts[lang] = lang_counts.get(lang, 0) + count
|
|
196
|
+
languages = sorted(lang_counts.keys())
|
|
197
|
+
# Find the top language that we have extension mappings for
|
|
198
|
+
ranked = sorted(lang_counts, key=lang_counts.get, reverse=True) # type: ignore[arg-type]
|
|
199
|
+
for lang in ranked:
|
|
200
|
+
if lang in LANGUAGE_EXTENSIONS:
|
|
201
|
+
code_pattern = _code_glob(LANGUAGE_EXTENSIONS[lang])
|
|
202
|
+
break
|
|
203
|
+
except Exception:
|
|
204
|
+
log.debug("project_overview inference failed", exc_info=True)
|
|
205
|
+
|
|
206
|
+
# ── Doc pattern ─────────────────────────────────────────────
|
|
207
|
+
doc_dir = _find_doc_dir(con, root or ".")
|
|
208
|
+
doc_pattern = str(Path(doc_dir) / "**" / "*.md") if doc_dir else "**/*.md"
|
|
209
|
+
|
|
210
|
+
# ── Main branch ─────────────────────────────────────────────
|
|
211
|
+
main_branch = _infer_main_branch(root or ".")
|
|
212
|
+
|
|
213
|
+
# ── Build defaults, apply overrides ─────────────────────────
|
|
214
|
+
defaults = ProjectDefaults(
|
|
215
|
+
code_pattern=code_pattern,
|
|
216
|
+
doc_pattern=doc_pattern,
|
|
217
|
+
main_branch=main_branch,
|
|
218
|
+
languages=languages,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
for key, value in overrides.items():
|
|
222
|
+
if hasattr(defaults, key):
|
|
223
|
+
setattr(defaults, key, value)
|
|
224
|
+
|
|
225
|
+
return defaults
|
squawkit/formatting.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Shared formatting and truncation helpers for fledgling.
|
|
2
|
+
|
|
3
|
+
Used by both server.py (auto-registered tools) and workflows.py
|
|
4
|
+
(compound tools) without circular imports.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# ── Truncation config ──────────────────────────────────────────────
|
|
11
|
+
# These dicts live here so _truncate_rows can reference them without
|
|
12
|
+
# importing server.py.
|
|
13
|
+
|
|
14
|
+
_MAX_LINES = {
|
|
15
|
+
"read_source": 200,
|
|
16
|
+
"read_context": 50,
|
|
17
|
+
"file_diff": 300,
|
|
18
|
+
"file_at_version": 200,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_MAX_ROWS = {
|
|
22
|
+
"find_definitions": 50,
|
|
23
|
+
"find_in_ast": 50,
|
|
24
|
+
"list_files": 100,
|
|
25
|
+
"doc_outline": 50,
|
|
26
|
+
"file_changes": 25,
|
|
27
|
+
"recent_changes": 20,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_HINTS = {
|
|
31
|
+
"read_source": "Use lines='N-M' to see a range, or match='keyword' to filter.",
|
|
32
|
+
"read_context": "Use a smaller context window or match='keyword' to filter.",
|
|
33
|
+
"file_diff": "Use a narrower revision range.",
|
|
34
|
+
"file_at_version": "Use lines='N-M' to see a range.",
|
|
35
|
+
"find_definitions": "Use name_pattern='%keyword%' to narrow, or file_pattern to scope.",
|
|
36
|
+
"find_in_ast": "Use name='keyword' to narrow results.",
|
|
37
|
+
"list_files": "Use a more specific glob pattern.",
|
|
38
|
+
"doc_outline": "Use search='keyword' to filter.",
|
|
39
|
+
"file_changes": "Use a narrower revision range.",
|
|
40
|
+
"recent_changes": "Use a smaller count.",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_HEAD_TAIL = 5 # rows to show at each end of truncated output
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _truncate_rows(rows, max_rows, macro_name):
|
|
47
|
+
"""Truncate rows to head + tail with an omission message.
|
|
48
|
+
|
|
49
|
+
Returns (display_rows, omission_line) where omission_line is None
|
|
50
|
+
if no truncation occurred.
|
|
51
|
+
"""
|
|
52
|
+
total = len(rows)
|
|
53
|
+
if max_rows <= 0 or total <= max_rows:
|
|
54
|
+
return rows, None
|
|
55
|
+
# Not enough rows for a clean head/tail split — return all
|
|
56
|
+
if total <= 2 * _HEAD_TAIL:
|
|
57
|
+
return rows, None
|
|
58
|
+
head = rows[:_HEAD_TAIL]
|
|
59
|
+
tail = rows[-_HEAD_TAIL:]
|
|
60
|
+
omitted = total - 2 * _HEAD_TAIL
|
|
61
|
+
hint = _HINTS.get(macro_name, "")
|
|
62
|
+
unit = "lines" if macro_name in _MAX_LINES else "rows"
|
|
63
|
+
msg = f"--- omitted {omitted} of {total} {unit} ---"
|
|
64
|
+
if hint:
|
|
65
|
+
msg += f"\n{hint}"
|
|
66
|
+
return head + tail, msg
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _format_markdown_table(cols: list[str], rows: list[tuple]) -> str:
|
|
70
|
+
"""Format query results as a markdown table."""
|
|
71
|
+
# Calculate column widths
|
|
72
|
+
widths = [len(c) for c in cols]
|
|
73
|
+
str_rows = []
|
|
74
|
+
for row in rows:
|
|
75
|
+
str_row = [str(v) if v is not None else "" for v in row]
|
|
76
|
+
str_rows.append(str_row)
|
|
77
|
+
for i, v in enumerate(str_row):
|
|
78
|
+
widths[i] = max(widths[i], len(v))
|
|
79
|
+
|
|
80
|
+
# Build table
|
|
81
|
+
lines = []
|
|
82
|
+
header = "| " + " | ".join(c.ljust(widths[i]) for i, c in enumerate(cols)) + " |"
|
|
83
|
+
sep = "|-" + "-|-".join("-" * widths[i] for i in range(len(cols))) + "-|"
|
|
84
|
+
lines.append(header)
|
|
85
|
+
lines.append(sep)
|
|
86
|
+
for str_row in str_rows:
|
|
87
|
+
line = "| " + " | ".join(v.ljust(widths[i]) for i, v in enumerate(str_row)) + " |"
|
|
88
|
+
lines.append(line)
|
|
89
|
+
return "\n".join(lines)
|
squawkit/prompts.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Fledgling: MCP prompt templates.
|
|
2
|
+
|
|
3
|
+
Parameterized workflow instructions with live project data pre-filled.
|
|
4
|
+
Each prompt calls the corresponding P4-004 workflow function to gather
|
|
5
|
+
context, then embeds it into condensed instructions derived from the
|
|
6
|
+
skill guides in skills/.
|
|
7
|
+
|
|
8
|
+
The agent uses prompts to learn *how* to approach a task; it uses
|
|
9
|
+
compound tools to get the *information* for the task.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from squawkit.workflows import explore, investigate, review
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from fastmcp import FastMCP
|
|
21
|
+
from duckdb import DuckDBPyConnection as Connection
|
|
22
|
+
from squawkit.defaults import ProjectDefaults
|
|
23
|
+
|
|
24
|
+
log = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _escape_braces(value: str) -> str:
|
|
28
|
+
"""Escape curly braces so user input is safe for str.format()."""
|
|
29
|
+
return value.replace("{", "{{").replace("}", "}}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ── Templates ─────────────────────────────────────────────────────
|
|
33
|
+
# Condensed from skills/*.md — actionable steps with {briefing}
|
|
34
|
+
# placeholder for live data and tool suggestions pre-filled with
|
|
35
|
+
# the project's inferred patterns.
|
|
36
|
+
|
|
37
|
+
EXPLORE_TEMPLATE = """\
|
|
38
|
+
## Explore Codebase
|
|
39
|
+
|
|
40
|
+
You are exploring {scope}. Below is a pre-loaded briefing followed by \
|
|
41
|
+
a workflow for deeper exploration.
|
|
42
|
+
|
|
43
|
+
### Project Briefing
|
|
44
|
+
|
|
45
|
+
{briefing}
|
|
46
|
+
|
|
47
|
+
### Exploration Workflow
|
|
48
|
+
|
|
49
|
+
Work top-down through these phases:
|
|
50
|
+
|
|
51
|
+
**Phase 1: Landscape** (loaded above)
|
|
52
|
+
Review the Languages, Key Definitions, and Documentation sections. \
|
|
53
|
+
Note the dominant language and most complex definitions.
|
|
54
|
+
|
|
55
|
+
**Phase 2: Architecture**
|
|
56
|
+
- `CodeStructure('{code_pattern}')` — definitions per file, complexity
|
|
57
|
+
- `FindDefinitions('{code_pattern}')` — functions, classes, modules
|
|
58
|
+
|
|
59
|
+
**Phase 3: Dependencies**
|
|
60
|
+
- `FindInAST('{code_pattern}', 'imports')` — external dependencies
|
|
61
|
+
- `FindInAST('{code_pattern}', 'calls')` — internal call graph
|
|
62
|
+
|
|
63
|
+
**Phase 4: History**
|
|
64
|
+
- `recent_changes(20)` — commit history
|
|
65
|
+
- `GitDiffSummary(from_rev='HEAD~10', to_rev='HEAD')` — changed files
|
|
66
|
+
- `changed_function_summary('HEAD~10', 'HEAD', '{code_pattern}')` — semantic changes
|
|
67
|
+
|
|
68
|
+
**Phase 5: Deep Dive**
|
|
69
|
+
- `ReadLines(file_path='...', lines='42-60')` — specific range
|
|
70
|
+
- `MDSection(file_path='...', section_id='...')` — doc sections
|
|
71
|
+
|
|
72
|
+
### Anti-Patterns
|
|
73
|
+
- Use `ReadLines` with line ranges, not `cat` or `head`
|
|
74
|
+
- Use `FindDefinitions` then `ReadLines`, not `grep -r`
|
|
75
|
+
- Use `list_files` with globs, not `find . -name`
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
INVESTIGATE_TEMPLATE = """\
|
|
79
|
+
## Investigate Issue
|
|
80
|
+
|
|
81
|
+
You are investigating: **{symptom}**
|
|
82
|
+
|
|
83
|
+
### Initial Findings
|
|
84
|
+
|
|
85
|
+
{briefing}
|
|
86
|
+
|
|
87
|
+
### Investigation Workflow
|
|
88
|
+
|
|
89
|
+
**Step 1: Locate** (findings above)
|
|
90
|
+
Review the definitions and source above. If not found, try:
|
|
91
|
+
- `FindDefinitions('{code_pattern}', '%{symptom}%')` — broader search
|
|
92
|
+
- `ReadLines(file_path='...', match='{symptom}')` — grep-like search
|
|
93
|
+
|
|
94
|
+
**Step 2: Understand the Code**
|
|
95
|
+
- `ReadLines(file_path='...', lines='42', ctx='10')` — context around a line
|
|
96
|
+
- `CodeStructure('...')` — file overview with complexity
|
|
97
|
+
|
|
98
|
+
**Step 3: Check History**
|
|
99
|
+
- `GitDiffSummary(from_rev='HEAD~20', to_rev='HEAD')` — what files changed
|
|
100
|
+
- `GitDiffFile(file='...', from_rev='HEAD~5', to_rev='HEAD')` — line-level diff
|
|
101
|
+
- `changed_function_summary('HEAD~10', 'HEAD', '{code_pattern}')` — complexity changes
|
|
102
|
+
|
|
103
|
+
**Step 4: Trace Dependencies**
|
|
104
|
+
- `FindInAST('...', 'imports')` — external dependencies
|
|
105
|
+
- `FindInAST('...', 'calls')` — function calls made
|
|
106
|
+
|
|
107
|
+
**Step 5: Check Related Code**
|
|
108
|
+
- `FindDefinitions('{code_pattern}', '%similar_name%')` — related functions
|
|
109
|
+
- `function_callers('{code_pattern}', 'func_name')` — callers
|
|
110
|
+
|
|
111
|
+
### Key Principles
|
|
112
|
+
- Locate before reading — find the right file and line first
|
|
113
|
+
- History is data — check what changed recently
|
|
114
|
+
- Compose queries — use the query tool for complex joins
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
REVIEW_TEMPLATE = """\
|
|
118
|
+
## Review Changes
|
|
119
|
+
|
|
120
|
+
You are reviewing: **{rev_range}**
|
|
121
|
+
|
|
122
|
+
### Change Summary
|
|
123
|
+
|
|
124
|
+
{briefing}
|
|
125
|
+
|
|
126
|
+
### Review Checklist
|
|
127
|
+
|
|
128
|
+
**Step 1: File-Level Overview** (loaded above)
|
|
129
|
+
Review Changed Files. Prioritize largest changes; note new/deleted files.
|
|
130
|
+
|
|
131
|
+
**Step 2: Function-Level Analysis** (loaded above)
|
|
132
|
+
Review Changed Functions. Focus on:
|
|
133
|
+
- High-complexity new functions (need most scrutiny)
|
|
134
|
+
- Functions where complexity increased
|
|
135
|
+
- Deleted functions (check for orphaned callers)
|
|
136
|
+
|
|
137
|
+
**Step 3: Read Diffs** (top diffs loaded above)
|
|
138
|
+
For additional files:
|
|
139
|
+
- `GitDiffFile(file='...', from_rev='{from_rev}', to_rev='{to_rev}')`
|
|
140
|
+
|
|
141
|
+
**Step 4: Understand Context**
|
|
142
|
+
- `ReadLines(file_path='...', lines='42', ctx='15')` — surrounding code
|
|
143
|
+
- `CodeStructure('...')` — file structure
|
|
144
|
+
|
|
145
|
+
**Step 5: Check Impact**
|
|
146
|
+
- `function_callers('{code_pattern}', 'func_name')` — callers of changed functions
|
|
147
|
+
- `FindInAST('...', 'imports')` — dependency changes
|
|
148
|
+
|
|
149
|
+
**Step 6: Compare Versions**
|
|
150
|
+
- `GitShow(file='...', rev='{from_rev}')` — old implementation
|
|
151
|
+
|
|
152
|
+
### Review Quality Checks
|
|
153
|
+
- New functions without tests? Check `changed_function_summary` where change_status='added'
|
|
154
|
+
- Complexity increases above 5? Inspect those functions
|
|
155
|
+
- Large new files (>5000 bytes)? Consider splitting
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ── Registration ──────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def register_prompts(mcp: FastMCP, con: Connection, defaults: ProjectDefaults):
|
|
163
|
+
"""Register MCP prompt templates on the FastMCP server."""
|
|
164
|
+
|
|
165
|
+
@mcp.prompt(
|
|
166
|
+
name="explore",
|
|
167
|
+
description="Exploration workflow with live project data: languages, "
|
|
168
|
+
"key definitions, docs, recent activity, and step-by-step "
|
|
169
|
+
"guidance for deeper exploration.",
|
|
170
|
+
)
|
|
171
|
+
def explore_prompt(path: str | None = None) -> str:
|
|
172
|
+
scope = f"path: {path}" if path else "the full project"
|
|
173
|
+
code_pattern = (
|
|
174
|
+
defaults.scoped_code_pattern(path)
|
|
175
|
+
if path else defaults.code_pattern
|
|
176
|
+
)
|
|
177
|
+
# Escape user input for safe str.format() interpolation
|
|
178
|
+
scope = _escape_braces(scope)
|
|
179
|
+
code_pattern = _escape_braces(code_pattern)
|
|
180
|
+
try:
|
|
181
|
+
briefing = explore(con, defaults, path=path)
|
|
182
|
+
except Exception:
|
|
183
|
+
log.debug("explore prompt: data gathering failed", exc_info=True)
|
|
184
|
+
briefing = "(Project data could not be loaded. Use the tools below to gather context manually.)"
|
|
185
|
+
|
|
186
|
+
return EXPLORE_TEMPLATE.format(
|
|
187
|
+
scope=scope,
|
|
188
|
+
briefing=briefing,
|
|
189
|
+
code_pattern=code_pattern,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
@mcp.prompt(
|
|
193
|
+
name="investigate",
|
|
194
|
+
description="Investigation workflow with pre-found definitions and "
|
|
195
|
+
"source code for a symptom (error message, function name, "
|
|
196
|
+
"or file path), plus step-by-step debugging guidance.",
|
|
197
|
+
)
|
|
198
|
+
def investigate_prompt(symptom: str) -> str:
|
|
199
|
+
code_pattern = defaults.code_pattern
|
|
200
|
+
try:
|
|
201
|
+
briefing = investigate(con, defaults, name=symptom)
|
|
202
|
+
except Exception:
|
|
203
|
+
log.debug("investigate prompt: data gathering failed", exc_info=True)
|
|
204
|
+
briefing = f"(Could not find data for '{symptom}'. Use the tools below to search manually.)"
|
|
205
|
+
|
|
206
|
+
return INVESTIGATE_TEMPLATE.format(
|
|
207
|
+
symptom=_escape_braces(symptom),
|
|
208
|
+
briefing=briefing,
|
|
209
|
+
code_pattern=_escape_braces(code_pattern),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
@mcp.prompt(
|
|
213
|
+
name="review",
|
|
214
|
+
description="Code review checklist with pre-loaded change summary, "
|
|
215
|
+
"complexity deltas, and diffs, plus step-by-step review "
|
|
216
|
+
"guidance.",
|
|
217
|
+
)
|
|
218
|
+
def review_prompt(from_rev: str | None = None, to_rev: str | None = None) -> str:
|
|
219
|
+
effective_from = from_rev or defaults.from_rev
|
|
220
|
+
effective_to = to_rev or defaults.to_rev
|
|
221
|
+
rev_range = f"{effective_from}..{effective_to}"
|
|
222
|
+
code_pattern = defaults.code_pattern
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
briefing = review(con, defaults, from_rev=from_rev, to_rev=to_rev)
|
|
226
|
+
except Exception:
|
|
227
|
+
log.debug("review prompt: data gathering failed", exc_info=True)
|
|
228
|
+
briefing = "(Change data could not be loaded. Use the tools below to gather context manually.)"
|
|
229
|
+
|
|
230
|
+
return REVIEW_TEMPLATE.format(
|
|
231
|
+
rev_range=rev_range,
|
|
232
|
+
briefing=briefing,
|
|
233
|
+
from_rev=effective_from,
|
|
234
|
+
to_rev=effective_to,
|
|
235
|
+
code_pattern=code_pattern,
|
|
236
|
+
)
|