squackit 0.8.1__tar.gz → 0.8.2__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.8.1 → squackit-0.8.2}/CHANGELOG.md +36 -0
- {squackit-0.8.1 → squackit-0.8.2}/PKG-INFO +1 -1
- {squackit-0.8.1 → squackit-0.8.2}/pyproject.toml +1 -1
- {squackit-0.8.1 → squackit-0.8.2}/squackit/server.py +22 -4
- {squackit-0.8.1 → squackit-0.8.2}/squackit/tools.py +112 -9
- {squackit-0.8.1 → squackit-0.8.2}/tests/test_truncation.py +5 -0
- {squackit-0.8.1 → squackit-0.8.2}/tests/test_workflows.py +16 -2
- {squackit-0.8.1 → squackit-0.8.2}/.github/workflows/release.yml +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/.gitignore +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/.mcp.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/.readthedocs.yaml +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/.readthedocs.yml +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/CLAUDE.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/LICENSE +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/README.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/agent-system-prompt.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/architecture.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/configuration.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/index.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/prompts.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/proposals/2026-06-04-response-type-improvements.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/quickstart.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/resources.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/plans/2026-04-10-phase1-handoff.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/plans/2026-04-10-squawkit-extraction.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/plans/2026-04-11-phase3-pluckit-rewire.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/plans/2026-04-12-cli-tool-exposure.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/plans/2026-04-12-pluckit-tool-integration.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/plans/2026-04-12-session-handoff.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/plans/2026-04-16-session-handoff.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/plans/2026-04-26-named-fts-collections.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/plans/2026-04-27-kibitzer-fts-doc-retrieval.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/skills/exploring-unfamiliar-code/SKILL.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/skills/exploring-unfamiliar-docs/SKILL.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/skills/pluck-chains/SKILL.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/skills/refactoring-with-squackit/SKILL.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/skills/using-squackit/SKILL.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/specs/2026-04-10-squawkit-design.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/specs/2026-04-12-cli-tool-exposure-design.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/specs/2026-04-12-pluckit-tool-integration-design.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/specs/2026-04-16-training-data-pipeline-design.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/specs/2026-04-26-named-fts-collections-design.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/docs/tools.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/mkdocs.yml +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/EXPERIMENT_LOG.md +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/benchmark_models.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/cascade_prototype.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/combo_eval.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/generate_examples.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/grid_generate_examples.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/hard_task_results/deepseek-coder-v2_16b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/hard_task_results/qwen2.5-coder_0.5b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/hard_task_results/qwen2.5-coder_3b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/hard_task_results/qwen2.5-coder_7b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/hard_task_results/qwen3.5_2b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/hard_task_results/qwen3_8b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/hard_task_results/scoring_input.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/hard_task_results/summary.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/hard_tasks.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/harvest_grid.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/iterative_eval.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/iterative_results/summary.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/iterative_results/summary_deepseek_plain.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/iterative_results/summary_distilled_xml.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/iterative_results/summary_editor_3b_7b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/iterative_results/summary_fulldocs.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/iterative_results/summary_plaintext_3b7b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/codegemma_2b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/codegemma_7b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/deepseek-coder-v2_16b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/deepseek-llm_7b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/gemma_latest.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/glm4_9b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/granite-code_3b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/granite3.1-dense_2b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/granite3.3_2b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/llama2-uncensored_7b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/llama3.2-vision_11b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/llama3.2_1b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/llama3.2_latest.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/phi3_latest.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/phi4-mini_latest.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/qwen2.5-coder_0.5b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/qwen2.5-coder_1.5b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/qwen2.5-coder_3b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/qwen2.5-coder_7b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/qwen2.5_1.5b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/qwen2.5_3b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/qwen2.5_7b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/qwen2_latest.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/qwen3.5_0.8b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/qwen3.5_2b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/qwen3.5_4b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/qwen3.5_9b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/qwen3_0.6b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/qwen3_8b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/smollm2_latest.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_card_results/summary.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/model_cards.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/multiline_results/deepseek-coder-v2_16b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/multiline_results/qwen2.5-coder_3b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/multiline_results/qwen2.5-coder_7b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/multiline_results/qwen3.5_2b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/multiline_results/qwen3_8b.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/multiline_results/summary.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/multiline_tasks.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/qm_retrieval_eval.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/quartermaster_eval.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/quartermaster_results/retrieval_summary.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/quartermaster_results/summary.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/selector_eval.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/scripts/selector_results/summary.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/squackit/__init__.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/squackit/__main__.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/squackit/cli.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/squackit/data/examples.json +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/squackit/db.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/squackit/defaults.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/squackit/formatting.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/squackit/lackpy_integration.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/squackit/prompts.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/squackit/runtime.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/squackit/session.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/squackit/tool_config.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/squackit/workflows.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/tests/conftest.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/tests/fixtures/has_call_filter.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/tests/test_cli_tools.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/tests/test_config_consumption.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/tests/test_defaults.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/tests/test_formatting_json.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/tests/test_fts_root.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/tests/test_prompts.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/tests/test_resources.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/tests/test_runtime_config.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/tests/test_sandbox_anchoring.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/tests/test_session.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/tests/test_smoke.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/tests/test_tool_config.py +0 -0
- {squackit-0.8.1 → squackit-0.8.2}/tests/test_tools.py +0 -0
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.2
|
|
4
|
+
|
|
5
|
+
### Fixed — AST tools could not read a source outside the working directory
|
|
6
|
+
|
|
7
|
+
`find` / `view` / `find_names` / `complexity` build their own connection with
|
|
8
|
+
no root, so fledgling's sandbox confined them to the process working
|
|
9
|
+
directory. An absolute source anywhere else — `find(source="/srv/code/x/**/*.py")`
|
|
10
|
+
— raised a permission error. The connection is now rooted at the source's own
|
|
11
|
+
directory, which is the scope the caller named. Relative sources are untouched:
|
|
12
|
+
they already resolve against the working directory, and re-rooting them would
|
|
13
|
+
double-prefix the path.
|
|
14
|
+
|
|
15
|
+
Each rooted plucker gets its own AST cache file, keyed by root. DuckDB's
|
|
16
|
+
filesystem allow-list belongs to the database *instance*, not the connection,
|
|
17
|
+
and every connection to one file shares an instance — so a single shared cache
|
|
18
|
+
meant the first lockdown won for the life of the process and later roots were
|
|
19
|
+
silently ignored. The cache lives in the temp directory rather than inside the
|
|
20
|
+
root, since a served corpus is mounted read-only.
|
|
21
|
+
|
|
22
|
+
### Fixed — de-vendoring aborted the transaction and blamed the next query
|
|
23
|
+
|
|
24
|
+
When a glob's tree had no `.git`/`.gitmodules` ancestor, the walk up degraded
|
|
25
|
+
to `/` and `_submodule_prefixes('')` read the filesystem root — refused under
|
|
26
|
+
the sandbox. The failure was swallowed by design (de-vendoring is an
|
|
27
|
+
optimization, not a correctness gate), but the aborted DuckDB transaction was
|
|
28
|
+
not rolled back, so the *next* statement failed with "Current transaction is
|
|
29
|
+
aborted" — pointing at the AST cache, which had done nothing wrong. Submodule
|
|
30
|
+
exclusion is now skipped when there is no repository to read, and the fallback
|
|
31
|
+
rolls back so the connection stays usable.
|
|
32
|
+
|
|
33
|
+
### Fixed — `read_source` output was double-spaced
|
|
34
|
+
|
|
35
|
+
Rows carry each line's own trailing newline, and the text renderer joined them
|
|
36
|
+
with another, so every second line was blank. That also broke head/tail
|
|
37
|
+
truncation, whose omission message is inserted by row index.
|
|
38
|
+
|
|
3
39
|
## 0.8.1
|
|
4
40
|
|
|
5
41
|
### Fixed — every code query returned "not found" under fledgling >= 0.13
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: squackit
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
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
|
|
@@ -296,7 +296,12 @@ def _register_executor_tool(mcp, presentation: ToolPresentation):
|
|
|
296
296
|
if is_text:
|
|
297
297
|
lines = []
|
|
298
298
|
for row in rows:
|
|
299
|
-
|
|
299
|
+
# Strip each value's own line terminator: this branch joins
|
|
300
|
+
# rows with "\n" itself, so a source line that already ends
|
|
301
|
+
# in one renders double-spaced, every second line blank.
|
|
302
|
+
# Only \r and \n come off — leading indentation is content,
|
|
303
|
+
# and so is any trailing space a line genuinely carries.
|
|
304
|
+
parts = [str(v).rstrip("\r\n") for v in row if v is not None]
|
|
300
305
|
lines.append(" ".join(parts))
|
|
301
306
|
if omission:
|
|
302
307
|
lines.append(omission)
|
|
@@ -594,16 +599,29 @@ def _register_tool(
|
|
|
594
599
|
|
|
595
600
|
# Format output
|
|
596
601
|
if is_text:
|
|
602
|
+
def _cell(value) -> str:
|
|
603
|
+
"""Render one value as a line fragment.
|
|
604
|
+
|
|
605
|
+
Drops the value's own line terminator: this branch joins rows
|
|
606
|
+
with "\\n" itself, and `read_source` returns each line's content
|
|
607
|
+
WITH its trailing newline. Keeping both renders the file
|
|
608
|
+
double-spaced — every second line blank — which also breaks
|
|
609
|
+
head/tail truncation's line accounting, since the omission
|
|
610
|
+
message is inserted by row index. Only \\r and \\n come off;
|
|
611
|
+
leading indentation is content and must survive.
|
|
612
|
+
"""
|
|
613
|
+
return str(value).rstrip("\r\n") if value is not None else ""
|
|
614
|
+
|
|
597
615
|
if len(cols) == 1:
|
|
598
|
-
lines = [
|
|
616
|
+
lines = [_cell(r[0]) for r in rows]
|
|
599
617
|
elif "line_number" in cols and "content" in cols:
|
|
600
618
|
ln_idx = cols.index("line_number")
|
|
601
619
|
ct_idx = cols.index("content")
|
|
602
|
-
lines = [f"{r[ln_idx]:4d} {r[ct_idx]}" for r in rows]
|
|
620
|
+
lines = [f"{r[ln_idx]:4d} {_cell(r[ct_idx])}" for r in rows]
|
|
603
621
|
else:
|
|
604
622
|
lines = []
|
|
605
623
|
for row in rows:
|
|
606
|
-
parts = [
|
|
624
|
+
parts = [_cell(v) for v in row if v is not None]
|
|
607
625
|
lines.append(" ".join(parts))
|
|
608
626
|
if omission:
|
|
609
627
|
lines.insert(_HEAD_TAIL, omission)
|
|
@@ -8,7 +8,10 @@ fledgling macro tools. They take priority over fledgling equivalents
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import hashlib
|
|
11
12
|
import os
|
|
13
|
+
import re
|
|
14
|
+
import tempfile
|
|
12
15
|
|
|
13
16
|
from fledgling.tools import ToolInfo
|
|
14
17
|
from squackit.tool_config import ToolPresentation
|
|
@@ -79,27 +82,41 @@ def _filtered_source(plucker, source: str, peek: str = DEFAULT_PEEK) -> str:
|
|
|
79
82
|
# prefix (the path before the first wildcard) and walk up to the nearest
|
|
80
83
|
# .gitmodules / .git. The denylist is absolute-pattern based, no root needed.
|
|
81
84
|
prefix = resolved.split("*", 1)[0]
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
candidate = prefix if prefix.endswith("/") else os.path.dirname(prefix)
|
|
86
|
+
# Walk up to the repository that owns these files. If there ISN'T one,
|
|
87
|
+
# leave root empty and skip submodule exclusion entirely: there are no
|
|
88
|
+
# submodules to exclude, and the walk otherwise degrades to "/", whose
|
|
89
|
+
# rstrip("/") is the empty string. `_submodule_prefixes('')` then reads
|
|
90
|
+
# the filesystem root — which a sandboxed connection refuses, aborting
|
|
91
|
+
# the transaction and taking down every later query on this connection.
|
|
92
|
+
root = ""
|
|
93
|
+
while candidate and candidate != "/":
|
|
94
|
+
if os.path.exists(os.path.join(candidate, ".gitmodules")) or os.path.exists(
|
|
95
|
+
os.path.join(candidate, ".git")
|
|
86
96
|
):
|
|
97
|
+
root = candidate
|
|
87
98
|
break
|
|
88
|
-
|
|
99
|
+
candidate = os.path.dirname(candidate)
|
|
89
100
|
rq = resolved.replace("'", "''")
|
|
90
|
-
rootq = root.rstrip("/").replace("'", "''")
|
|
91
101
|
|
|
92
102
|
total = db.sql(f"SELECT count(*) FROM glob('{rq}')").fetchone()[0]
|
|
93
103
|
if total <= 1:
|
|
94
104
|
return source
|
|
95
105
|
|
|
106
|
+
submodule_clause = ""
|
|
107
|
+
if root:
|
|
108
|
+
rootq = root.rstrip("/").replace("'", "''")
|
|
109
|
+
submodule_clause = (
|
|
110
|
+
f"AND NOT EXISTS (SELECT 1 FROM _submodule_prefixes('{rootq}') s "
|
|
111
|
+
f"WHERE file LIKE s.prefix || '%') "
|
|
112
|
+
)
|
|
113
|
+
|
|
96
114
|
kept = [
|
|
97
115
|
r[0]
|
|
98
116
|
for r in db.sql(
|
|
99
117
|
f"SELECT file FROM glob('{rq}') "
|
|
100
118
|
f"WHERE NOT _is_vendored_path(file) "
|
|
101
|
-
f"
|
|
102
|
-
f"WHERE file LIKE s.prefix || '%') "
|
|
119
|
+
f"{submodule_clause}"
|
|
103
120
|
f"ORDER BY file"
|
|
104
121
|
).fetchall()
|
|
105
122
|
]
|
|
@@ -115,9 +132,81 @@ def _filtered_source(plucker, source: str, peek: str = DEFAULT_PEEK) -> str:
|
|
|
115
132
|
except Exception:
|
|
116
133
|
# Ignore-policy macros unavailable / SQL error — fall back to the raw
|
|
117
134
|
# glob. Unfiltered (flooded) results beat a broken tool.
|
|
135
|
+
#
|
|
136
|
+
# ROLL BACK FIRST. A failed statement leaves the DuckDB transaction in
|
|
137
|
+
# an aborted state, so swallowing the error without clearing it poisons
|
|
138
|
+
# every later query on this connection with "Current transaction is
|
|
139
|
+
# aborted (please ROLLBACK)" — including the AST cache lookup that runs
|
|
140
|
+
# moments later. The caller then sees a transaction error from a
|
|
141
|
+
# statement that never failed, pointing at the wrong subsystem entirely.
|
|
142
|
+
# Degrading gracefully means the connection is still usable afterwards,
|
|
143
|
+
# which is the whole promise of this except clause.
|
|
144
|
+
try:
|
|
145
|
+
db = plucker._ctx.db
|
|
146
|
+
if db is not None:
|
|
147
|
+
db.execute("ROLLBACK")
|
|
148
|
+
except Exception:
|
|
149
|
+
pass # no transaction open, or the connection is beyond saving
|
|
118
150
|
return source
|
|
119
151
|
|
|
120
152
|
|
|
153
|
+
def _source_root(source: str) -> str | None:
|
|
154
|
+
"""The directory that must be readable for *source* to resolve.
|
|
155
|
+
|
|
156
|
+
A fledgling connection is sandboxed to its root (fledgling >= 0.13), so a
|
|
157
|
+
Plucker built without one is confined to the process working directory and
|
|
158
|
+
cannot read a source anywhere else: ``find(source="/srv/code/x/**/*.py")``
|
|
159
|
+
raised a permission error instead of returning results. These tools take
|
|
160
|
+
their target as an argument, so the target is the scope — rooting the
|
|
161
|
+
connection at the source's own directory keeps the sandbox meaningful while
|
|
162
|
+
letting an explicit path resolve.
|
|
163
|
+
|
|
164
|
+
Returns None when there is nothing to root at (a bare table/view name, or a
|
|
165
|
+
directory that does not exist), leaving pluckit's default behaviour alone.
|
|
166
|
+
"""
|
|
167
|
+
if not source:
|
|
168
|
+
return None
|
|
169
|
+
# A bare identifier is a DuckDB table/view name, not a path.
|
|
170
|
+
if not any(sep in source for sep in ("/", os.sep)):
|
|
171
|
+
return None
|
|
172
|
+
# RELATIVE sources are already handled: they resolve against the working
|
|
173
|
+
# directory, which is what an un-rooted Plucker sandboxes to. Re-rooting
|
|
174
|
+
# them would be actively wrong — _filtered_source joins a relative source
|
|
175
|
+
# onto the connection's repo, so pointing that repo at the source's own
|
|
176
|
+
# directory turns `squackit/**/*.py` into `.../squackit/squackit/**/*.py`
|
|
177
|
+
# and the tool finds nothing.
|
|
178
|
+
if not os.path.isabs(source):
|
|
179
|
+
return None
|
|
180
|
+
head = re.split(r"[*?\[]", source, maxsplit=1)[0]
|
|
181
|
+
directory = head if head.endswith(("/", os.sep)) else os.path.dirname(head)
|
|
182
|
+
if not directory:
|
|
183
|
+
return None
|
|
184
|
+
directory = os.path.abspath(directory)
|
|
185
|
+
return directory if os.path.isdir(directory) else None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _cache_path(root: str | None) -> str:
|
|
189
|
+
"""Where the AST cache database lives for a Plucker rooted at *root*.
|
|
190
|
+
|
|
191
|
+
DuckDB's filesystem allow-list is a property of the database INSTANCE, not
|
|
192
|
+
of the connection, and every connection to the same file shares one
|
|
193
|
+
instance. A single shared cache file therefore means the FIRST lockdown
|
|
194
|
+
wins for the life of the process: a plucker rooted at /srv/code/x reuses
|
|
195
|
+
the instance already locked to the working directory and is refused access
|
|
196
|
+
to its own source, with no indication that its root was ignored.
|
|
197
|
+
|
|
198
|
+
So a rooted plucker gets its own cache file, keyed by the root. It goes in
|
|
199
|
+
the temp directory rather than inside the root: a served corpus is mounted
|
|
200
|
+
read-only, and writing a database into a tree we were only asked to read is
|
|
201
|
+
rude even where it succeeds. Keying by root keeps the cache reusable across
|
|
202
|
+
processes, which is the point of persisting it at all.
|
|
203
|
+
"""
|
|
204
|
+
if root is None:
|
|
205
|
+
return os.path.join(os.getcwd(), ".pluckit.duckdb")
|
|
206
|
+
digest = hashlib.sha256(os.path.abspath(root).encode()).hexdigest()[:16]
|
|
207
|
+
return os.path.join(tempfile.gettempdir(), f"squackit-ast-{digest}.duckdb")
|
|
208
|
+
|
|
209
|
+
|
|
121
210
|
def _make_plucker(source: str, peek: str = DEFAULT_PEEK):
|
|
122
211
|
"""Create a Plucker with AstViewer and Search for tool execution.
|
|
123
212
|
|
|
@@ -135,7 +224,21 @@ def _make_plucker(source: str, peek: str = DEFAULT_PEEK):
|
|
|
135
224
|
# parses with ``peek := 'none'`` and can only ever return NULL. Enabling the
|
|
136
225
|
# cache moves the un-filtered path onto ``ast_select_from`` too, so both
|
|
137
226
|
# paths return source text. It also stops re-parsing on every call.
|
|
138
|
-
|
|
227
|
+
#
|
|
228
|
+
# The cache path is passed EXPLICITLY rather than left to `cache=True`.
|
|
229
|
+
# pluckit derives the default cache file from the repo (`<repo>/.pluckit.duckdb`),
|
|
230
|
+
# so rooting the connection at the source would also move the cache into the
|
|
231
|
+
# tree being analysed — which for a served corpus is a read-only mount, and
|
|
232
|
+
# for anyone else means writing a database into a repo they only asked us to
|
|
233
|
+
# read. Keeping it under the working directory preserves the location this
|
|
234
|
+
# had before the connection gained a root.
|
|
235
|
+
root = _source_root(source)
|
|
236
|
+
p = Plucker(
|
|
237
|
+
plugins=[AstViewer, Search],
|
|
238
|
+
cache=_cache_path(root),
|
|
239
|
+
peek=peek,
|
|
240
|
+
repo=root,
|
|
241
|
+
)
|
|
139
242
|
p._code_source = _filtered_source(p, source, peek)
|
|
140
243
|
return p
|
|
141
244
|
|
|
@@ -279,7 +279,12 @@ class TestMarkdownTruncation:
|
|
|
279
279
|
assert omission_idx == 7 # 0:header, 1:sep, 2-6:head, 7:omission
|
|
280
280
|
|
|
281
281
|
@pytest.mark.anyio
|
|
282
|
+
@requires_fledgling_source
|
|
282
283
|
async def test_max_results_zero_disables(self, mcp):
|
|
284
|
+
# recent_changes queries git history, so the dog-food corpus has to be
|
|
285
|
+
# a repository. An installed wheel in site-packages has no .git and the
|
|
286
|
+
# macro raises "No git repository found" — a fact about the corpus, not
|
|
287
|
+
# about truncation.
|
|
283
288
|
result = _text(await mcp.call_tool("recent_changes", {
|
|
284
289
|
"max_results": 0,
|
|
285
290
|
}))
|
|
@@ -9,7 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
import pytest
|
|
11
11
|
|
|
12
|
-
from conftest import PROJECT_ROOT
|
|
12
|
+
from conftest import PROJECT_ROOT, requires_fledgling_source
|
|
13
13
|
|
|
14
14
|
try:
|
|
15
15
|
import fastmcp # noqa: F401
|
|
@@ -174,7 +174,18 @@ class TestExplore:
|
|
|
174
174
|
|
|
175
175
|
@requires_fastmcp
|
|
176
176
|
class TestInvestigate:
|
|
177
|
-
"""Test the investigate compound tool.
|
|
177
|
+
"""Test the investigate compound tool.
|
|
178
|
+
|
|
179
|
+
The section assertions below need a fledgling SOURCE checkout as the
|
|
180
|
+
dog-food corpus. Against an installed wheel the corpus is site-packages,
|
|
181
|
+
where the bundled sql/ outnumbers the Python, so the inferred code_pattern
|
|
182
|
+
is legitimately `**/*.sql` and investigating a Python symbol finds nothing
|
|
183
|
+
— the briefing then has no Definition/Source/Called-by sections. That is a
|
|
184
|
+
fact about the corpus, not about investigate, and asserting it against a
|
|
185
|
+
wheel sends anyone debugging it into squackit's scoping logic instead. The
|
|
186
|
+
tests that hold either way (registration, unknown-name handling, project
|
|
187
|
+
scoping) stay unmarked.
|
|
188
|
+
"""
|
|
178
189
|
|
|
179
190
|
@pytest.fixture(scope="class")
|
|
180
191
|
def text(self, mcp):
|
|
@@ -185,12 +196,15 @@ class TestInvestigate:
|
|
|
185
196
|
def test_returns_non_empty(self, text):
|
|
186
197
|
assert len(text) > 0
|
|
187
198
|
|
|
199
|
+
@requires_fledgling_source
|
|
188
200
|
def test_contains_definition_section(self, text):
|
|
189
201
|
assert "Definition" in text
|
|
190
202
|
|
|
203
|
+
@requires_fledgling_source
|
|
191
204
|
def test_contains_source_section(self, text):
|
|
192
205
|
assert "Source" in text
|
|
193
206
|
|
|
207
|
+
@requires_fledgling_source
|
|
194
208
|
def test_contains_called_by_section(self, text):
|
|
195
209
|
assert "Called by" in text
|
|
196
210
|
|
|
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.8.1 → squackit-0.8.2}/docs/superpowers/plans/2026-04-11-phase3-pluckit-rewire.md
RENAMED
|
File without changes
|
|
File without changes
|
{squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/plans/2026-04-12-pluckit-tool-integration.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/plans/2026-04-26-named-fts-collections.md
RENAMED
|
File without changes
|
{squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/plans/2026-04-27-kibitzer-fts-doc-retrieval.md
RENAMED
|
File without changes
|
{squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/skills/exploring-unfamiliar-code/SKILL.md
RENAMED
|
File without changes
|
{squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/skills/exploring-unfamiliar-docs/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
{squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/skills/refactoring-with-squackit/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/specs/2026-04-12-cli-tool-exposure-design.md
RENAMED
|
File without changes
|
|
File without changes
|
{squackit-0.8.1 → squackit-0.8.2}/docs/superpowers/specs/2026-04-16-training-data-pipeline-design.md
RENAMED
|
File without changes
|
{squackit-0.8.1 → squackit-0.8.2}/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
|
|
File without changes
|
|
File without changes
|