squackit 0.6.0__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {squackit-0.6.0 → squackit-0.7.0}/PKG-INFO +1 -1
- {squackit-0.6.0 → squackit-0.7.0}/pyproject.toml +1 -1
- squackit-0.7.0/squackit/runtime.py +167 -0
- {squackit-0.6.0 → squackit-0.7.0}/squackit/workflows.py +30 -3
- squackit-0.7.0/tests/test_runtime_config.py +175 -0
- {squackit-0.6.0 → squackit-0.7.0}/.github/workflows/release.yml +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/.gitignore +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/.mcp.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/.readthedocs.yaml +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/.readthedocs.yml +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/CHANGELOG.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/CLAUDE.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/LICENSE +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/README.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/agent-system-prompt.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/architecture.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/configuration.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/index.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/prompts.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/proposals/2026-06-04-response-type-improvements.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/quickstart.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/resources.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-10-phase1-handoff.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-10-squawkit-extraction.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-11-phase3-pluckit-rewire.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-12-cli-tool-exposure.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-12-pluckit-tool-integration.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-12-session-handoff.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-16-session-handoff.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-26-named-fts-collections.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-27-kibitzer-fts-doc-retrieval.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/skills/exploring-unfamiliar-code/SKILL.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/skills/exploring-unfamiliar-docs/SKILL.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/skills/pluck-chains/SKILL.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/skills/refactoring-with-squackit/SKILL.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/skills/using-squackit/SKILL.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/specs/2026-04-10-squawkit-design.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/specs/2026-04-12-cli-tool-exposure-design.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/specs/2026-04-12-pluckit-tool-integration-design.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/specs/2026-04-16-training-data-pipeline-design.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/specs/2026-04-26-named-fts-collections-design.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/docs/tools.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/mkdocs.yml +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/EXPERIMENT_LOG.md +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/benchmark_models.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/cascade_prototype.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/combo_eval.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/generate_examples.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/grid_generate_examples.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/hard_task_results/deepseek-coder-v2_16b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/hard_task_results/qwen2.5-coder_0.5b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/hard_task_results/qwen2.5-coder_3b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/hard_task_results/qwen2.5-coder_7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/hard_task_results/qwen3.5_2b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/hard_task_results/qwen3_8b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/hard_task_results/scoring_input.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/hard_task_results/summary.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/hard_tasks.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/harvest_grid.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/iterative_eval.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/iterative_results/summary.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/iterative_results/summary_deepseek_plain.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/iterative_results/summary_distilled_xml.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/iterative_results/summary_editor_3b_7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/iterative_results/summary_fulldocs.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/iterative_results/summary_plaintext_3b7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/codegemma_2b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/codegemma_7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/deepseek-coder-v2_16b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/deepseek-llm_7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/gemma_latest.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/glm4_9b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/granite-code_3b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/granite3.1-dense_2b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/granite3.3_2b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/llama2-uncensored_7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/llama3.2-vision_11b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/llama3.2_1b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/llama3.2_latest.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/phi3_latest.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/phi4-mini_latest.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/qwen2.5-coder_0.5b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/qwen2.5-coder_1.5b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/qwen2.5-coder_3b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/qwen2.5-coder_7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/qwen2.5_1.5b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/qwen2.5_3b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/qwen2.5_7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/qwen2_latest.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/qwen3.5_0.8b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/qwen3.5_2b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/qwen3.5_4b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/qwen3.5_9b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/qwen3_0.6b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/qwen3_8b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/smollm2_latest.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_card_results/summary.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/model_cards.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/multiline_results/deepseek-coder-v2_16b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/multiline_results/qwen2.5-coder_3b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/multiline_results/qwen2.5-coder_7b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/multiline_results/qwen3.5_2b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/multiline_results/qwen3_8b.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/multiline_results/summary.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/multiline_tasks.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/qm_retrieval_eval.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/quartermaster_eval.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/quartermaster_results/retrieval_summary.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/quartermaster_results/summary.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/selector_eval.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/scripts/selector_results/summary.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/squackit/__init__.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/squackit/__main__.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/squackit/cli.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/squackit/data/examples.json +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/squackit/db.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/squackit/defaults.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/squackit/formatting.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/squackit/lackpy_integration.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/squackit/prompts.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/squackit/server.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/squackit/session.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/squackit/tool_config.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/squackit/tools.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/tests/conftest.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/tests/test_cli_tools.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/tests/test_defaults.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/tests/test_formatting_json.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/tests/test_fts_root.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/tests/test_prompts.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/tests/test_resources.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/tests/test_session.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/tests/test_smoke.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/tests/test_tool_config.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/tests/test_tools.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/tests/test_truncation.py +0 -0
- {squackit-0.6.0 → squackit-0.7.0}/tests/test_workflows.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: squackit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
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
|
|
@@ -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__}")
|
|
@@ -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,175 @@
|
|
|
1
|
+
"""Tests for the in-memory runtime config + MCP config() tool."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from squackit import runtime as rt
|
|
8
|
+
from squackit.runtime import (
|
|
9
|
+
SquackitRuntimeConfig,
|
|
10
|
+
get_runtime,
|
|
11
|
+
reset_runtime,
|
|
12
|
+
resolve_scope_path,
|
|
13
|
+
update_runtime,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(autouse=True)
|
|
18
|
+
def _isolate_runtime():
|
|
19
|
+
"""Reset the module singleton + seed between tests so order doesn't matter."""
|
|
20
|
+
rt._runtime = SquackitRuntimeConfig()
|
|
21
|
+
rt._seed = SquackitRuntimeConfig()
|
|
22
|
+
yield
|
|
23
|
+
rt._runtime = SquackitRuntimeConfig()
|
|
24
|
+
rt._seed = SquackitRuntimeConfig()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestFromEnv:
|
|
28
|
+
def test_empty_env_returns_defaults(self):
|
|
29
|
+
cfg = SquackitRuntimeConfig.from_env(env={})
|
|
30
|
+
assert cfg.active_root is None
|
|
31
|
+
assert cfg.log_level == "info"
|
|
32
|
+
assert cfg.max_results_default == 50
|
|
33
|
+
assert cfg.complexity_max_results_default == 20
|
|
34
|
+
assert cfg.fts_cache_size == 8
|
|
35
|
+
|
|
36
|
+
def test_active_root_from_env(self):
|
|
37
|
+
cfg = SquackitRuntimeConfig.from_env(env={"SQUACKIT_ACTIVE_ROOT": "/tmp/repo"})
|
|
38
|
+
assert cfg.active_root == "/tmp/repo"
|
|
39
|
+
|
|
40
|
+
def test_log_level_normalized_lowercase(self):
|
|
41
|
+
cfg = SquackitRuntimeConfig.from_env(env={"SQUACKIT_LOG_LEVEL": "DEBUG"})
|
|
42
|
+
assert cfg.log_level == "debug"
|
|
43
|
+
|
|
44
|
+
def test_log_level_invalid_falls_back_to_default(self):
|
|
45
|
+
cfg = SquackitRuntimeConfig.from_env(env={"SQUACKIT_LOG_LEVEL": "verbose"})
|
|
46
|
+
assert cfg.log_level == "info"
|
|
47
|
+
|
|
48
|
+
def test_int_field_from_env(self):
|
|
49
|
+
cfg = SquackitRuntimeConfig.from_env(env={"SQUACKIT_MAX_RESULTS_DEFAULT": "100"})
|
|
50
|
+
assert cfg.max_results_default == 100
|
|
51
|
+
|
|
52
|
+
def test_int_field_invalid_falls_back(self):
|
|
53
|
+
cfg = SquackitRuntimeConfig.from_env(env={"SQUACKIT_MAX_RESULTS_DEFAULT": "abc"})
|
|
54
|
+
assert cfg.max_results_default == 50
|
|
55
|
+
|
|
56
|
+
def test_int_field_zero_clamped_to_one(self):
|
|
57
|
+
cfg = SquackitRuntimeConfig.from_env(env={"SQUACKIT_FTS_CACHE_SIZE": "0"})
|
|
58
|
+
assert cfg.fts_cache_size == 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestUpdateRuntime:
|
|
62
|
+
def test_set_active_root_persists_in_singleton(self):
|
|
63
|
+
update_runtime({"active_root": "/tmp/x"})
|
|
64
|
+
assert get_runtime().active_root == "/tmp/x"
|
|
65
|
+
|
|
66
|
+
def test_unknown_key_raises_and_leaves_unchanged(self):
|
|
67
|
+
original = get_runtime().to_dict()
|
|
68
|
+
with pytest.raises(ValueError, match="unknown config key"):
|
|
69
|
+
update_runtime({"bogus": "value"})
|
|
70
|
+
assert get_runtime().to_dict() == original
|
|
71
|
+
|
|
72
|
+
def test_invalid_value_raises_and_leaves_unchanged(self):
|
|
73
|
+
original = get_runtime().to_dict()
|
|
74
|
+
with pytest.raises(ValueError, match="log_level"):
|
|
75
|
+
update_runtime({"log_level": "verbose"})
|
|
76
|
+
assert get_runtime().to_dict() == original
|
|
77
|
+
|
|
78
|
+
def test_atomic_batch_set_either_all_or_none(self):
|
|
79
|
+
original = get_runtime().to_dict()
|
|
80
|
+
with pytest.raises(ValueError):
|
|
81
|
+
update_runtime({"active_root": "/tmp/y", "log_level": "verbose"})
|
|
82
|
+
# active_root should NOT have been applied even though it's valid
|
|
83
|
+
assert get_runtime().to_dict() == original
|
|
84
|
+
|
|
85
|
+
def test_int_field_rejects_string(self):
|
|
86
|
+
with pytest.raises(ValueError, match="max_results_default"):
|
|
87
|
+
update_runtime({"max_results_default": "50"})
|
|
88
|
+
|
|
89
|
+
def test_int_field_rejects_bool(self):
|
|
90
|
+
# bool is an int subclass in Python — must reject explicitly
|
|
91
|
+
with pytest.raises(ValueError, match="max_results_default"):
|
|
92
|
+
update_runtime({"max_results_default": True})
|
|
93
|
+
|
|
94
|
+
def test_int_field_rejects_zero_or_negative(self):
|
|
95
|
+
with pytest.raises(ValueError, match="fts_cache_size"):
|
|
96
|
+
update_runtime({"fts_cache_size": 0})
|
|
97
|
+
with pytest.raises(ValueError, match="fts_cache_size"):
|
|
98
|
+
update_runtime({"fts_cache_size": -1})
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TestResetRuntime:
|
|
102
|
+
def test_reset_reverts_to_seed(self, monkeypatch):
|
|
103
|
+
# Simulate launch-time env seed
|
|
104
|
+
monkeypatch.setenv("SQUACKIT_ACTIVE_ROOT", "/tmp/seeded")
|
|
105
|
+
rt._seed = SquackitRuntimeConfig.from_env()
|
|
106
|
+
rt._runtime = SquackitRuntimeConfig.from_env()
|
|
107
|
+
# Override at runtime
|
|
108
|
+
update_runtime({"active_root": "/tmp/overridden"})
|
|
109
|
+
assert get_runtime().active_root == "/tmp/overridden"
|
|
110
|
+
# Reset goes back to seed (not dataclass defaults)
|
|
111
|
+
reset_runtime()
|
|
112
|
+
assert get_runtime().active_root == "/tmp/seeded"
|
|
113
|
+
|
|
114
|
+
def test_reset_with_no_env_goes_to_defaults(self):
|
|
115
|
+
update_runtime({"active_root": "/tmp/x", "max_results_default": 200})
|
|
116
|
+
reset_runtime()
|
|
117
|
+
assert get_runtime().active_root is None
|
|
118
|
+
assert get_runtime().max_results_default == 50
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TestResolveScopePath:
|
|
122
|
+
"""Precedence: explicit -> runtime.active_root -> os.getcwd()."""
|
|
123
|
+
|
|
124
|
+
def test_explicit_wins(self, monkeypatch):
|
|
125
|
+
update_runtime({"active_root": "/tmp/runtime"})
|
|
126
|
+
monkeypatch.chdir("/tmp")
|
|
127
|
+
assert resolve_scope_path("/tmp/explicit") == "/tmp/explicit"
|
|
128
|
+
|
|
129
|
+
def test_runtime_active_root_used_when_explicit_none(self, monkeypatch):
|
|
130
|
+
update_runtime({"active_root": "/tmp/runtime"})
|
|
131
|
+
monkeypatch.chdir("/tmp")
|
|
132
|
+
assert resolve_scope_path(None) == "/tmp/runtime"
|
|
133
|
+
|
|
134
|
+
def test_cwd_used_when_both_none(self, tmp_path, monkeypatch):
|
|
135
|
+
monkeypatch.chdir(tmp_path)
|
|
136
|
+
assert resolve_scope_path(None) == str(tmp_path)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class TestConfigToolViaMCP:
|
|
140
|
+
"""Smoke tests for the registered MCP tool."""
|
|
141
|
+
|
|
142
|
+
@pytest.fixture(scope="class")
|
|
143
|
+
def mcp(self):
|
|
144
|
+
pytest.importorskip("fastmcp")
|
|
145
|
+
from conftest import PROJECT_ROOT
|
|
146
|
+
from squackit.server import create_server
|
|
147
|
+
return create_server(root=PROJECT_ROOT, init=False)
|
|
148
|
+
|
|
149
|
+
def test_config_is_registered(self, mcp):
|
|
150
|
+
import asyncio
|
|
151
|
+
async def _list():
|
|
152
|
+
from fastmcp import Client
|
|
153
|
+
async with Client(mcp) as client:
|
|
154
|
+
tools = await client.list_tools()
|
|
155
|
+
return [t.name for t in tools]
|
|
156
|
+
loop = asyncio.new_event_loop()
|
|
157
|
+
try:
|
|
158
|
+
names = loop.run_until_complete(_list())
|
|
159
|
+
finally:
|
|
160
|
+
loop.close()
|
|
161
|
+
assert "config" in names
|
|
162
|
+
|
|
163
|
+
def test_config_returns_current_state(self, mcp):
|
|
164
|
+
import asyncio
|
|
165
|
+
async def _call():
|
|
166
|
+
return await mcp.call_tool("config", {})
|
|
167
|
+
loop = asyncio.new_event_loop()
|
|
168
|
+
try:
|
|
169
|
+
result = loop.run_until_complete(_call())
|
|
170
|
+
finally:
|
|
171
|
+
loop.close()
|
|
172
|
+
# FastMCP wraps the dict in result.content[0].text as JSON
|
|
173
|
+
text = result.content[0].text if hasattr(result, "content") else str(result)
|
|
174
|
+
assert "active_root" in text
|
|
175
|
+
assert "max_results_default" in text
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-11-phase3-pluckit-rewire.md
RENAMED
|
File without changes
|
|
File without changes
|
{squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-12-pluckit-tool-integration.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-26-named-fts-collections.md
RENAMED
|
File without changes
|
{squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-27-kibitzer-fts-doc-retrieval.md
RENAMED
|
File without changes
|
{squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/skills/exploring-unfamiliar-code/SKILL.md
RENAMED
|
File without changes
|
{squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/skills/exploring-unfamiliar-docs/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
{squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/skills/refactoring-with-squackit/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/specs/2026-04-12-cli-tool-exposure-design.md
RENAMED
|
File without changes
|
|
File without changes
|
{squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/specs/2026-04-16-training-data-pipeline-design.md
RENAMED
|
File without changes
|
{squackit-0.6.0 → squackit-0.7.0}/docs/superpowers/specs/2026-04-26-named-fts-collections-design.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|