squackit 0.6.0__tar.gz → 0.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {squackit-0.6.0 → squackit-0.7.1}/CHANGELOG.md +36 -0
- {squackit-0.6.0 → squackit-0.7.1}/PKG-INFO +4 -4
- {squackit-0.6.0 → squackit-0.7.1}/pyproject.toml +4 -4
- squackit-0.7.1/squackit/runtime.py +167 -0
- {squackit-0.6.0 → squackit-0.7.1}/squackit/server.py +10 -0
- {squackit-0.6.0 → squackit-0.7.1}/squackit/tools.py +96 -2
- {squackit-0.6.0 → squackit-0.7.1}/squackit/workflows.py +30 -3
- squackit-0.7.1/tests/fixtures/has_call_filter.py +23 -0
- squackit-0.7.1/tests/test_config_consumption.py +95 -0
- squackit-0.7.1/tests/test_runtime_config.py +175 -0
- {squackit-0.6.0 → squackit-0.7.1}/tests/test_tools.py +90 -0
- {squackit-0.6.0 → squackit-0.7.1}/.github/workflows/release.yml +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/.gitignore +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/.mcp.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/.readthedocs.yaml +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/.readthedocs.yml +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/CLAUDE.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/LICENSE +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/README.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/agent-system-prompt.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/architecture.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/configuration.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/index.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/prompts.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/proposals/2026-06-04-response-type-improvements.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/quickstart.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/resources.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-10-phase1-handoff.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-10-squawkit-extraction.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-11-phase3-pluckit-rewire.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-12-cli-tool-exposure.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-12-pluckit-tool-integration.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-12-session-handoff.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-16-session-handoff.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-26-named-fts-collections.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-27-kibitzer-fts-doc-retrieval.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/skills/exploring-unfamiliar-code/SKILL.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/skills/exploring-unfamiliar-docs/SKILL.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/skills/pluck-chains/SKILL.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/skills/refactoring-with-squackit/SKILL.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/skills/using-squackit/SKILL.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/specs/2026-04-10-squawkit-design.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/specs/2026-04-12-cli-tool-exposure-design.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/specs/2026-04-12-pluckit-tool-integration-design.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/specs/2026-04-16-training-data-pipeline-design.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/superpowers/specs/2026-04-26-named-fts-collections-design.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/docs/tools.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/mkdocs.yml +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/EXPERIMENT_LOG.md +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/benchmark_models.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/cascade_prototype.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/combo_eval.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/generate_examples.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/grid_generate_examples.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/hard_task_results/deepseek-coder-v2_16b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/hard_task_results/qwen2.5-coder_0.5b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/hard_task_results/qwen2.5-coder_3b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/hard_task_results/qwen2.5-coder_7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/hard_task_results/qwen3.5_2b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/hard_task_results/qwen3_8b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/hard_task_results/scoring_input.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/hard_task_results/summary.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/hard_tasks.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/harvest_grid.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/iterative_eval.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/iterative_results/summary.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/iterative_results/summary_deepseek_plain.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/iterative_results/summary_distilled_xml.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/iterative_results/summary_editor_3b_7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/iterative_results/summary_fulldocs.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/iterative_results/summary_plaintext_3b7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/codegemma_2b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/codegemma_7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/deepseek-coder-v2_16b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/deepseek-llm_7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/gemma_latest.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/glm4_9b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/granite-code_3b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/granite3.1-dense_2b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/granite3.3_2b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/llama2-uncensored_7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/llama3.2-vision_11b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/llama3.2_1b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/llama3.2_latest.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/phi3_latest.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/phi4-mini_latest.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/qwen2.5-coder_0.5b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/qwen2.5-coder_1.5b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/qwen2.5-coder_3b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/qwen2.5-coder_7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/qwen2.5_1.5b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/qwen2.5_3b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/qwen2.5_7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/qwen2_latest.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/qwen3.5_0.8b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/qwen3.5_2b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/qwen3.5_4b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/qwen3.5_9b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/qwen3_0.6b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/qwen3_8b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/smollm2_latest.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_card_results/summary.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/model_cards.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/multiline_results/deepseek-coder-v2_16b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/multiline_results/qwen2.5-coder_3b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/multiline_results/qwen2.5-coder_7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/multiline_results/qwen3.5_2b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/multiline_results/qwen3_8b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/multiline_results/summary.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/multiline_tasks.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/qm_retrieval_eval.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/quartermaster_eval.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/quartermaster_results/retrieval_summary.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/quartermaster_results/summary.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/selector_eval.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/scripts/selector_results/summary.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/squackit/__init__.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/squackit/__main__.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/squackit/cli.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/squackit/data/examples.json +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/squackit/db.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/squackit/defaults.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/squackit/formatting.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/squackit/lackpy_integration.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/squackit/prompts.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/squackit/session.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/squackit/tool_config.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/tests/conftest.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/tests/test_cli_tools.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/tests/test_defaults.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/tests/test_formatting_json.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/tests/test_fts_root.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/tests/test_prompts.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/tests/test_resources.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/tests/test_session.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/tests/test_smoke.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/tests/test_tool_config.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/tests/test_truncation.py +0 -0
- {squackit-0.6.0 → squackit-0.7.1}/tests/test_workflows.py +0 -0
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.1
|
|
4
|
+
|
|
5
|
+
### Added — server consumes default-limit knobs
|
|
6
|
+
`config()`'s `max_results_default` / `complexity_max_results_default` session
|
|
7
|
+
knobs are now honored by the server's tool truncation, so the in-memory limits
|
|
8
|
+
set via `config()` actually take effect.
|
|
9
|
+
|
|
10
|
+
### Changed — ast-pluckit 0.14 selector delegation
|
|
11
|
+
Consume ast-pluckit 0.14.0, which delegates CSS-selector matching to
|
|
12
|
+
sitting_duck's `ast_select` (fixes `:has(.call#name)` over-match, sitting_duck
|
|
13
|
+
#72 / squackit #8); pinned `ast-pluckit>=0.14,<0.15` with a regression test.
|
|
14
|
+
|
|
15
|
+
### Added — AST tools de-vendor the source glob (fledgling #47)
|
|
16
|
+
`find` / `find_names` / `view` / `complexity` now exclude submodule, build,
|
|
17
|
+
cache, and checked-in third-party trees from a whole-repo glob, so they focus on
|
|
18
|
+
the project's own code instead of drowning in vendored deps. On a DuckDB
|
|
19
|
+
extension with `duckdb/` + `rdkit/` git submodules, `find_names('**/*.cpp', '.function')`
|
|
20
|
+
went from **35k+ names parsed in ~9 s to 164 in ~2 s** (~660× less work at the
|
|
21
|
+
parse layer).
|
|
22
|
+
|
|
23
|
+
- Reuses fledgling's single-source-of-truth ignore policy (`_is_vendored_path`
|
|
24
|
+
denylist + `_submodule_prefixes` git-awareness, new in fledgling 0.12) — the
|
|
25
|
+
filtered file set is handed to sitting_duck as an explicit `read_ast` list,
|
|
26
|
+
which parses identically to a glob (verified same results), so only *which*
|
|
27
|
+
files are parsed changes, not how selectors match.
|
|
28
|
+
- The repo root for submodule exclusion is derived from the **source glob**
|
|
29
|
+
(walking up to the nearest `.gitmodules`/`.git`), not the server's cwd, so it
|
|
30
|
+
works when an agent queries a different project by absolute path.
|
|
31
|
+
- Explicit targets are honored: a single-file source, a DuckDB table name, or a
|
|
32
|
+
glob aimed *into* a vendored/submodule tree (e.g. `rdkit/**/*.cpp`) is passed
|
|
33
|
+
through unfiltered rather than filtered to zero.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
- Require `fledgling-mcp>=0.12` for the `_is_vendored_path` / `_submodule_prefixes`
|
|
37
|
+
ignore-policy macros.
|
|
38
|
+
|
|
3
39
|
## 0.6.0
|
|
4
40
|
|
|
5
41
|
### Added — `investigate(path=)` for cross-project scoping
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: squackit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Semi-QUalified Agent Companion Kit — the stateful intelligence + MCP server layer for fledgling-equipped agents.
|
|
5
5
|
Project-URL: Homepage, https://github.com/teaguesterling/squackit
|
|
6
6
|
Project-URL: Documentation, https://squackit.readthedocs.io
|
|
@@ -15,11 +15,11 @@ Classifier: Intended Audience :: Developers
|
|
|
15
15
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
17
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
-
Requires-Python: >=3.
|
|
19
|
-
Requires-Dist: ast-pluckit<0.
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: ast-pluckit<0.15,>=0.14
|
|
20
20
|
Requires-Dist: click>=8.0
|
|
21
21
|
Requires-Dist: fastmcp>=3.0
|
|
22
|
-
Requires-Dist: fledgling-mcp<0.
|
|
22
|
+
Requires-Dist: fledgling-mcp<0.13,>=0.12
|
|
23
23
|
Provides-Extra: docs
|
|
24
24
|
Requires-Dist: mkdocs-exclude>=1.0; extra == 'docs'
|
|
25
25
|
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "squackit"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.1"
|
|
4
4
|
description = "Semi-QUalified Agent Companion Kit — the stateful intelligence + MCP server layer for fledgling-equipped agents."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
7
|
-
requires-python = ">=3.
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name = "Teague Sterling" },
|
|
10
10
|
]
|
|
@@ -17,8 +17,8 @@ classifiers = [
|
|
|
17
17
|
"License :: OSI Approved :: Apache Software License",
|
|
18
18
|
]
|
|
19
19
|
dependencies = [
|
|
20
|
-
"ast-pluckit>=0.
|
|
21
|
-
"fledgling-mcp>=0.
|
|
20
|
+
"ast-pluckit>=0.14,<0.15",
|
|
21
|
+
"fledgling-mcp>=0.12,<0.13",
|
|
22
22
|
"click>=8.0",
|
|
23
23
|
"fastmcp>=3.0",
|
|
24
24
|
]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""In-memory runtime configuration for the squackit MCP server.
|
|
2
|
+
|
|
3
|
+
Distinct from ProjectDefaults (which is inferred-at-startup + file-loaded
|
|
4
|
+
overrides from .fledgling-python/config.toml). This module holds *session*
|
|
5
|
+
state that the MCP `config()` tool reads and writes — wiped on server
|
|
6
|
+
restart, no disk persistence.
|
|
7
|
+
|
|
8
|
+
Env vars at launch seed the initial values (e.g. SQUACKIT_ACTIVE_ROOT set
|
|
9
|
+
in .mcp.json env block). Resetting reverts to those env-seeded values, not
|
|
10
|
+
hardcoded defaults.
|
|
11
|
+
|
|
12
|
+
Shape mirrors jetsam.config.runtime; see retritis bench/config-endpoint-plan.md
|
|
13
|
+
for the cross-suite design.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
from dataclasses import dataclass, field, fields, replace
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_ENV_PREFIX = "SQUACKIT_"
|
|
24
|
+
|
|
25
|
+
_VALID_LOG_LEVELS = {"debug", "info", "warn", "warning", "error"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class SquackitRuntimeConfig:
|
|
30
|
+
"""Session-scoped runtime knobs for the squackit MCP server.
|
|
31
|
+
|
|
32
|
+
Common keys (suite-wide convention, also on jetsam):
|
|
33
|
+
active_root: fallback when a tool's `path=`/`root=` arg is omitted.
|
|
34
|
+
None means use the server's process cwd (today's behavior).
|
|
35
|
+
log_level: debug | info | warn | error.
|
|
36
|
+
|
|
37
|
+
Squackit-specific:
|
|
38
|
+
max_results_default: default cap for token-aware result truncation
|
|
39
|
+
when tools don't get an explicit max_results.
|
|
40
|
+
complexity_max_results_default: separate cap for `complexity` tool,
|
|
41
|
+
which is usually requested in smaller batches.
|
|
42
|
+
fts_cache_size: LRU size for per-root FTS indexes. Larger = more
|
|
43
|
+
roots cached cheaply; smaller = less RAM but more rebuilds.
|
|
44
|
+
Note: takes effect on NEW roots; existing cache entries keep
|
|
45
|
+
their slot until they age out naturally.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
active_root: str | None = None
|
|
49
|
+
log_level: str = "info"
|
|
50
|
+
max_results_default: int = 50
|
|
51
|
+
complexity_max_results_default: int = 20
|
|
52
|
+
fts_cache_size: int = 8
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_env(cls, env: dict[str, str] | None = None) -> "SquackitRuntimeConfig":
|
|
56
|
+
"""Build a config seeded from environment variables.
|
|
57
|
+
|
|
58
|
+
Variables read (each optional):
|
|
59
|
+
SQUACKIT_ACTIVE_ROOT
|
|
60
|
+
SQUACKIT_LOG_LEVEL
|
|
61
|
+
SQUACKIT_MAX_RESULTS_DEFAULT
|
|
62
|
+
SQUACKIT_COMPLEXITY_MAX_RESULTS_DEFAULT
|
|
63
|
+
SQUACKIT_FTS_CACHE_SIZE
|
|
64
|
+
|
|
65
|
+
Invalid values fall back to the dataclass default.
|
|
66
|
+
"""
|
|
67
|
+
e = env if env is not None else os.environ
|
|
68
|
+
cfg = cls()
|
|
69
|
+
|
|
70
|
+
if v := e.get(f"{_ENV_PREFIX}ACTIVE_ROOT"):
|
|
71
|
+
cfg.active_root = v
|
|
72
|
+
if v := e.get(f"{_ENV_PREFIX}LOG_LEVEL"):
|
|
73
|
+
if v.lower() in _VALID_LOG_LEVELS:
|
|
74
|
+
cfg.log_level = v.lower()
|
|
75
|
+
if v := e.get(f"{_ENV_PREFIX}MAX_RESULTS_DEFAULT"):
|
|
76
|
+
try:
|
|
77
|
+
cfg.max_results_default = max(1, int(v))
|
|
78
|
+
except ValueError:
|
|
79
|
+
pass
|
|
80
|
+
if v := e.get(f"{_ENV_PREFIX}COMPLEXITY_MAX_RESULTS_DEFAULT"):
|
|
81
|
+
try:
|
|
82
|
+
cfg.complexity_max_results_default = max(1, int(v))
|
|
83
|
+
except ValueError:
|
|
84
|
+
pass
|
|
85
|
+
if v := e.get(f"{_ENV_PREFIX}FTS_CACHE_SIZE"):
|
|
86
|
+
try:
|
|
87
|
+
cfg.fts_cache_size = max(1, int(v))
|
|
88
|
+
except ValueError:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
return cfg
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> dict[str, Any]:
|
|
94
|
+
"""Flat dict for the MCP `config()` response."""
|
|
95
|
+
return {f.name: getattr(self, f.name) for f in fields(self)}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Module-level singleton, seeded from env on import.
|
|
99
|
+
_runtime: SquackitRuntimeConfig = SquackitRuntimeConfig.from_env()
|
|
100
|
+
_seed: SquackitRuntimeConfig = replace(_runtime)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_runtime() -> SquackitRuntimeConfig:
|
|
104
|
+
"""Return the current runtime config."""
|
|
105
|
+
return _runtime
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def update_runtime(values: dict[str, Any]) -> SquackitRuntimeConfig:
|
|
109
|
+
"""Merge values into the runtime config. Validates atomically.
|
|
110
|
+
|
|
111
|
+
Raises ValueError on unknown keys or invalid values, leaving the runtime
|
|
112
|
+
config unchanged.
|
|
113
|
+
"""
|
|
114
|
+
global _runtime
|
|
115
|
+
valid_keys = {f.name for f in fields(SquackitRuntimeConfig)}
|
|
116
|
+
unknown = set(values) - valid_keys
|
|
117
|
+
if unknown:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
f"unknown config key(s): {sorted(unknown)}. "
|
|
120
|
+
f"Valid keys: {sorted(valid_keys)}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
candidate = replace(_runtime)
|
|
124
|
+
for key, value in values.items():
|
|
125
|
+
_validate_one(key, value)
|
|
126
|
+
setattr(candidate, key, value)
|
|
127
|
+
|
|
128
|
+
_runtime = candidate
|
|
129
|
+
return _runtime
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def reset_runtime() -> SquackitRuntimeConfig:
|
|
133
|
+
"""Reset to the env-seeded values captured at module import time."""
|
|
134
|
+
global _runtime
|
|
135
|
+
_runtime = replace(_seed)
|
|
136
|
+
return _runtime
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def resolve_scope_path(explicit_path: str | None = None) -> str:
|
|
140
|
+
"""Resolve a scope path with the runtime config fallback chain.
|
|
141
|
+
|
|
142
|
+
Precedence: explicit_path -> runtime.active_root -> process cwd.
|
|
143
|
+
Used by tools that scope queries to a directory (investigate, etc.)
|
|
144
|
+
so a single `config(set={"active_root": X})` call configures the whole
|
|
145
|
+
session.
|
|
146
|
+
"""
|
|
147
|
+
if explicit_path is not None:
|
|
148
|
+
return explicit_path
|
|
149
|
+
runtime_root = _runtime.active_root
|
|
150
|
+
if runtime_root is not None:
|
|
151
|
+
return runtime_root
|
|
152
|
+
return os.getcwd()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _validate_one(key: str, value: Any) -> None:
|
|
156
|
+
"""Per-key value validation."""
|
|
157
|
+
if key == "log_level":
|
|
158
|
+
if not isinstance(value, str) or value.lower() not in _VALID_LOG_LEVELS:
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"log_level must be one of {sorted(_VALID_LOG_LEVELS)}, got {value!r}"
|
|
161
|
+
)
|
|
162
|
+
elif key in ("max_results_default", "complexity_max_results_default", "fts_cache_size"):
|
|
163
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 1:
|
|
164
|
+
raise ValueError(f"{key} must be a positive int, got {value!r}")
|
|
165
|
+
elif key == "active_root":
|
|
166
|
+
if value is not None and not isinstance(value, str):
|
|
167
|
+
raise ValueError(f"active_root must be str or None, got {type(value).__name__}")
|
|
@@ -225,6 +225,16 @@ def _register_executor_tool(mcp, presentation: ToolPresentation):
|
|
|
225
225
|
max_limit = int(val)
|
|
226
226
|
except (TypeError, ValueError):
|
|
227
227
|
pass
|
|
228
|
+
else:
|
|
229
|
+
# Caller passed limit=None — fall back to runtime config.
|
|
230
|
+
# `complexity` is its own bucket; everything else uses the
|
|
231
|
+
# general default_max_results.
|
|
232
|
+
from squackit.runtime import get_runtime
|
|
233
|
+
rt = get_runtime()
|
|
234
|
+
if tool_name == "complexity":
|
|
235
|
+
max_limit = rt.complexity_max_results_default
|
|
236
|
+
elif limit_param == "max_results":
|
|
237
|
+
max_limit = rt.max_results_default
|
|
228
238
|
|
|
229
239
|
# Pop verbose param the same way — opts out of compact_columns
|
|
230
240
|
# projection so the caller can see the full AST bookkeeping schema.
|
|
@@ -8,16 +8,110 @@ fledgling macro tools. They take priority over fledgling equivalents
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import os
|
|
12
|
+
|
|
11
13
|
from fledgling.tools import ToolInfo
|
|
12
14
|
from squackit.tool_config import ToolPresentation
|
|
13
15
|
|
|
14
16
|
|
|
17
|
+
def _filtered_source(plucker, source: str) -> str:
|
|
18
|
+
"""De-vendor a source glob before it reaches pluckit's AST engine.
|
|
19
|
+
|
|
20
|
+
A bare ``**/*`` glob over a repo with git submodules or checked-in
|
|
21
|
+
third-party trees (googletest, vendored deps) makes the AST tools parse
|
|
22
|
+
thousands of files that aren't the project's own code — the ``find`` /
|
|
23
|
+
``view`` / ``complexity`` flood. We reuse fledgling's single-source-of-truth
|
|
24
|
+
ignore policy (``_is_vendored_path`` denylist + ``_submodule_prefixes``
|
|
25
|
+
git-awareness) to expand the glob, drop the vendored files, and hand pluckit
|
|
26
|
+
an explicit ``read_ast`` table of just the survivors via its existing
|
|
27
|
+
table-name source path. sitting_duck parses a list of paths exactly as it
|
|
28
|
+
parses a glob (verified identical results), so this changes only *which*
|
|
29
|
+
files are parsed, not how selectors match.
|
|
30
|
+
|
|
31
|
+
Returns the original ``source`` unchanged — so pluckit handles it directly —
|
|
32
|
+
when filtering doesn't apply or would be wrong:
|
|
33
|
+
* not a glob (a single file path or a DuckDB table/view name) — an
|
|
34
|
+
explicit target, nothing to de-flood;
|
|
35
|
+
* the glob expands to <= 1 file;
|
|
36
|
+
* filtering removes *everything* — the caller deliberately aimed at a
|
|
37
|
+
vendored/submodule tree (e.g. ``rdkit/**/*.cpp``), so honor it;
|
|
38
|
+
* filtering removes *nothing* — passing the glob through is cheaper and
|
|
39
|
+
gives identical results.
|
|
40
|
+
Otherwise returns the name of a TEMP table holding the filtered AST.
|
|
41
|
+
|
|
42
|
+
Degrades gracefully: if the fledgling ignore-policy macros can't run for any
|
|
43
|
+
reason (not loaded, SQL error), we fall back to the raw ``source`` — the
|
|
44
|
+
caller gets the historical unfiltered (flooded) behavior rather than a dead
|
|
45
|
+
tool. This is a de-flooding optimization, not a correctness gate, so a raw
|
|
46
|
+
glob is a safe answer; never let it take down find/view/complexity.
|
|
47
|
+
"""
|
|
48
|
+
if "*" not in source:
|
|
49
|
+
return source
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
db = plucker._ctx.db
|
|
53
|
+
resolved = (
|
|
54
|
+
source if os.path.isabs(source)
|
|
55
|
+
else os.path.join(plucker._ctx.repo, source)
|
|
56
|
+
)
|
|
57
|
+
# Submodule exclusion reads <root>/.gitmodules, so root must be the repo
|
|
58
|
+
# that actually contains the globbed files — NOT the plucker's cwd, which
|
|
59
|
+
# may be a different project entirely. Derive it from the glob's literal
|
|
60
|
+
# prefix (the path before the first wildcard) and walk up to the nearest
|
|
61
|
+
# .gitmodules / .git. The denylist is absolute-pattern based, no root needed.
|
|
62
|
+
prefix = resolved.split("*", 1)[0]
|
|
63
|
+
root = prefix if prefix.endswith("/") else os.path.dirname(prefix)
|
|
64
|
+
while root and root != "/":
|
|
65
|
+
if os.path.exists(os.path.join(root, ".gitmodules")) or os.path.exists(
|
|
66
|
+
os.path.join(root, ".git")
|
|
67
|
+
):
|
|
68
|
+
break
|
|
69
|
+
root = os.path.dirname(root)
|
|
70
|
+
rq = resolved.replace("'", "''")
|
|
71
|
+
rootq = root.rstrip("/").replace("'", "''")
|
|
72
|
+
|
|
73
|
+
total = db.sql(f"SELECT count(*) FROM glob('{rq}')").fetchone()[0]
|
|
74
|
+
if total <= 1:
|
|
75
|
+
return source
|
|
76
|
+
|
|
77
|
+
kept = [
|
|
78
|
+
r[0]
|
|
79
|
+
for r in db.sql(
|
|
80
|
+
f"SELECT file FROM glob('{rq}') "
|
|
81
|
+
f"WHERE NOT _is_vendored_path(file) "
|
|
82
|
+
f"AND NOT EXISTS (SELECT 1 FROM _submodule_prefixes('{rootq}') s "
|
|
83
|
+
f"WHERE file LIKE s.prefix || '%') "
|
|
84
|
+
f"ORDER BY file"
|
|
85
|
+
).fetchall()
|
|
86
|
+
]
|
|
87
|
+
if not kept or len(kept) == total:
|
|
88
|
+
return source
|
|
89
|
+
|
|
90
|
+
lit = "[" + ", ".join("'" + f.replace("'", "''") + "'" for f in kept) + "]"
|
|
91
|
+
db.execute(
|
|
92
|
+
"CREATE OR REPLACE TEMP TABLE _squackit_src AS "
|
|
93
|
+
f"SELECT * FROM read_ast({lit}, peek := 'none+schema')"
|
|
94
|
+
)
|
|
95
|
+
return "_squackit_src"
|
|
96
|
+
except Exception:
|
|
97
|
+
# Ignore-policy macros unavailable / SQL error — fall back to the raw
|
|
98
|
+
# glob. Unfiltered (flooded) results beat a broken tool.
|
|
99
|
+
return source
|
|
100
|
+
|
|
101
|
+
|
|
15
102
|
def _make_plucker(source: str):
|
|
16
|
-
"""Create a Plucker with AstViewer and Search for tool execution.
|
|
103
|
+
"""Create a Plucker with AstViewer and Search for tool execution.
|
|
104
|
+
|
|
105
|
+
The source glob is de-vendored first (see :func:`_filtered_source`) so the
|
|
106
|
+
AST tools focus on the project's own code instead of drowning in submodules,
|
|
107
|
+
build output, and checked-in third-party trees.
|
|
108
|
+
"""
|
|
17
109
|
from pluckit import Plucker
|
|
18
110
|
from pluckit.pluckins.search import Search
|
|
19
111
|
from pluckit.pluckins.viewer import AstViewer
|
|
20
|
-
|
|
112
|
+
p = Plucker(plugins=[AstViewer, Search])
|
|
113
|
+
p._code_source = _filtered_source(p, source)
|
|
114
|
+
return p
|
|
21
115
|
|
|
22
116
|
|
|
23
117
|
def view_executor(*, source: str, selector: str):
|
|
@@ -145,9 +145,11 @@ def investigate(con, defaults, name, file_pattern=None, path=None):
|
|
|
145
145
|
Only consulted when file_pattern is None.
|
|
146
146
|
"""
|
|
147
147
|
if file_pattern is None:
|
|
148
|
-
import
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
from squackit.runtime import resolve_scope_path
|
|
149
|
+
# Precedence: explicit path -> runtime.active_root -> process cwd.
|
|
150
|
+
# Runtime fallback lets a session set active_root once via
|
|
151
|
+
# `config(set={"active_root": X})` instead of passing path= per call.
|
|
152
|
+
file_pattern = defaults.scoped_code_pattern(resolve_scope_path(path))
|
|
151
153
|
|
|
152
154
|
# 1. Find definitions matching the name
|
|
153
155
|
try:
|
|
@@ -456,3 +458,28 @@ def register_workflows(mcp, con, defaults):
|
|
|
456
458
|
("query", str, _empty),
|
|
457
459
|
("file_pattern", Optional[str], None),
|
|
458
460
|
])
|
|
461
|
+
|
|
462
|
+
async def config_tool(*, set=None, reset=False):
|
|
463
|
+
from squackit.runtime import get_runtime, reset_runtime, update_runtime
|
|
464
|
+
if reset:
|
|
465
|
+
cfg = reset_runtime()
|
|
466
|
+
return cfg.to_dict()
|
|
467
|
+
if set:
|
|
468
|
+
try:
|
|
469
|
+
cfg = update_runtime(set)
|
|
470
|
+
except ValueError as e:
|
|
471
|
+
return {"error": "invalid_config", "message": str(e)}
|
|
472
|
+
return cfg.to_dict()
|
|
473
|
+
return get_runtime().to_dict()
|
|
474
|
+
|
|
475
|
+
_add_workflow_tool(mcp, "config",
|
|
476
|
+
"Read or update squackit's in-memory session config. Keys: active_root "
|
|
477
|
+
"(fallback for tools that take path=/root=), log_level, max_results_default, "
|
|
478
|
+
"complexity_max_results_default, fts_cache_size. In-memory only — wiped on "
|
|
479
|
+
"server restart. Env-var seeded at launch (SQUACKIT_ACTIVE_ROOT, etc). "
|
|
480
|
+
"config() reads; config(set={...}) merges atomically; config(reset=true) "
|
|
481
|
+
"reverts to env seed. Mirrors jetsam's config() endpoint.",
|
|
482
|
+
config_tool, [
|
|
483
|
+
("set", Optional[dict], None),
|
|
484
|
+
("reset", bool, False),
|
|
485
|
+
])
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Fixture for the :has(.call#name) regression test.
|
|
2
|
+
|
|
3
|
+
Four top-level functions; exactly two call `_target`. Used to pin that
|
|
4
|
+
`:has(.call#_target)` applies the nested `#_target` filter rather than
|
|
5
|
+
dropping it and matching every function (sitting_duck #72 / squackit #8).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _target():
|
|
10
|
+
return 1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def calls_target_once():
|
|
14
|
+
return _target()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def calls_target_twice():
|
|
18
|
+
a = _target()
|
|
19
|
+
return a + _target()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def calls_other():
|
|
23
|
+
return len([])
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Tests for tools consuming runtime config knobs (max_results_default)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from squackit import runtime as rt
|
|
8
|
+
from squackit.runtime import SquackitRuntimeConfig, update_runtime
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture(autouse=True)
|
|
12
|
+
def _isolate_runtime():
|
|
13
|
+
rt._runtime = SquackitRuntimeConfig()
|
|
14
|
+
rt._seed = SquackitRuntimeConfig()
|
|
15
|
+
yield
|
|
16
|
+
rt._runtime = SquackitRuntimeConfig()
|
|
17
|
+
rt._seed = SquackitRuntimeConfig()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestMaxResultsDefault:
|
|
21
|
+
"""When a tool is called with max_results=None, the runtime config's
|
|
22
|
+
max_results_default should be used as the cap (not the presentation's
|
|
23
|
+
static max_rows)."""
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def mcp(self):
|
|
27
|
+
pytest.importorskip("fastmcp")
|
|
28
|
+
from conftest import PROJECT_ROOT
|
|
29
|
+
from squackit.server import create_server
|
|
30
|
+
return create_server(root=PROJECT_ROOT, init=False)
|
|
31
|
+
|
|
32
|
+
def _call(self, mcp, name, args):
|
|
33
|
+
import asyncio
|
|
34
|
+
from fastmcp import Client
|
|
35
|
+
|
|
36
|
+
async def _c():
|
|
37
|
+
async with Client(mcp) as client:
|
|
38
|
+
return await client.call_tool(name, args)
|
|
39
|
+
|
|
40
|
+
loop = asyncio.new_event_loop()
|
|
41
|
+
try:
|
|
42
|
+
return loop.run_until_complete(_c())
|
|
43
|
+
finally:
|
|
44
|
+
loop.close()
|
|
45
|
+
|
|
46
|
+
def test_explicit_max_results_still_wins(self, mcp):
|
|
47
|
+
update_runtime({"max_results_default": 5})
|
|
48
|
+
# call find_names with explicit max_results=3 — runtime knob ignored
|
|
49
|
+
result = self._call(mcp, "find_names", {
|
|
50
|
+
"source": "**/*.py", "selector": ".fn", "max_results": 3,
|
|
51
|
+
})
|
|
52
|
+
text = result.content[0].text if hasattr(result, "content") else str(result)
|
|
53
|
+
# Just verify the call succeeded — cap is a runtime concern, the
|
|
54
|
+
# important thing is the call completes and returns content.
|
|
55
|
+
assert len(text) > 0
|
|
56
|
+
|
|
57
|
+
def test_runtime_default_picked_up_when_omitted(self, mcp):
|
|
58
|
+
# Set a distinctive default so we can tell it took effect
|
|
59
|
+
update_runtime({"max_results_default": 7})
|
|
60
|
+
result = self._call(mcp, "find_names", {
|
|
61
|
+
"source": "**/*.py", "selector": ".fn",
|
|
62
|
+
})
|
|
63
|
+
text = result.content[0].text if hasattr(result, "content") else str(result)
|
|
64
|
+
assert len(text) > 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestComplexityMaxResults:
|
|
68
|
+
"""complexity gets its own knob since it's usually called in smaller batches."""
|
|
69
|
+
|
|
70
|
+
@pytest.fixture
|
|
71
|
+
def mcp(self):
|
|
72
|
+
pytest.importorskip("fastmcp")
|
|
73
|
+
from conftest import PROJECT_ROOT
|
|
74
|
+
from squackit.server import create_server
|
|
75
|
+
return create_server(root=PROJECT_ROOT, init=False)
|
|
76
|
+
|
|
77
|
+
def test_complexity_uses_its_own_knob(self, mcp):
|
|
78
|
+
import asyncio
|
|
79
|
+
from fastmcp import Client
|
|
80
|
+
|
|
81
|
+
update_runtime({"complexity_max_results_default": 3})
|
|
82
|
+
|
|
83
|
+
async def _c():
|
|
84
|
+
async with Client(mcp) as client:
|
|
85
|
+
return await client.call_tool("complexity", {
|
|
86
|
+
"source": "**/*.py", "selector": ".fn",
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
loop = asyncio.new_event_loop()
|
|
90
|
+
try:
|
|
91
|
+
result = loop.run_until_complete(_c())
|
|
92
|
+
finally:
|
|
93
|
+
loop.close()
|
|
94
|
+
text = result.content[0].text if hasattr(result, "content") else str(result)
|
|
95
|
+
assert len(text) > 0
|