squackit 0.8.2__tar.gz → 0.8.3__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.
Files changed (142) hide show
  1. {squackit-0.8.2 → squackit-0.8.3}/CHANGELOG.md +62 -0
  2. {squackit-0.8.2 → squackit-0.8.3}/PKG-INFO +1 -1
  3. {squackit-0.8.2 → squackit-0.8.3}/pyproject.toml +1 -1
  4. squackit-0.8.3/squackit/__init__.py +17 -0
  5. {squackit-0.8.2 → squackit-0.8.3}/squackit/tool_config.py +32 -1
  6. {squackit-0.8.2 → squackit-0.8.3}/squackit/tools.py +17 -4
  7. {squackit-0.8.2 → squackit-0.8.3}/squackit/workflows.py +18 -1
  8. squackit-0.8.3/tests/test_error_visibility.py +198 -0
  9. {squackit-0.8.2 → squackit-0.8.3}/tests/test_smoke.py +17 -1
  10. squackit-0.8.2/squackit/__init__.py +0 -3
  11. {squackit-0.8.2 → squackit-0.8.3}/.github/workflows/release.yml +0 -0
  12. {squackit-0.8.2 → squackit-0.8.3}/.gitignore +0 -0
  13. {squackit-0.8.2 → squackit-0.8.3}/.mcp.json +0 -0
  14. {squackit-0.8.2 → squackit-0.8.3}/.readthedocs.yaml +0 -0
  15. {squackit-0.8.2 → squackit-0.8.3}/.readthedocs.yml +0 -0
  16. {squackit-0.8.2 → squackit-0.8.3}/CLAUDE.md +0 -0
  17. {squackit-0.8.2 → squackit-0.8.3}/LICENSE +0 -0
  18. {squackit-0.8.2 → squackit-0.8.3}/README.md +0 -0
  19. {squackit-0.8.2 → squackit-0.8.3}/docs/agent-system-prompt.md +0 -0
  20. {squackit-0.8.2 → squackit-0.8.3}/docs/architecture.md +0 -0
  21. {squackit-0.8.2 → squackit-0.8.3}/docs/configuration.md +0 -0
  22. {squackit-0.8.2 → squackit-0.8.3}/docs/index.md +0 -0
  23. {squackit-0.8.2 → squackit-0.8.3}/docs/prompts.md +0 -0
  24. {squackit-0.8.2 → squackit-0.8.3}/docs/proposals/2026-06-04-response-type-improvements.md +0 -0
  25. {squackit-0.8.2 → squackit-0.8.3}/docs/quickstart.md +0 -0
  26. {squackit-0.8.2 → squackit-0.8.3}/docs/resources.md +0 -0
  27. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/plans/2026-04-10-phase1-handoff.md +0 -0
  28. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/plans/2026-04-10-squawkit-extraction.md +0 -0
  29. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/plans/2026-04-11-phase3-pluckit-rewire.md +0 -0
  30. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/plans/2026-04-12-cli-tool-exposure.md +0 -0
  31. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/plans/2026-04-12-pluckit-tool-integration.md +0 -0
  32. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/plans/2026-04-12-session-handoff.md +0 -0
  33. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/plans/2026-04-16-session-handoff.md +0 -0
  34. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/plans/2026-04-26-named-fts-collections.md +0 -0
  35. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/plans/2026-04-27-kibitzer-fts-doc-retrieval.md +0 -0
  36. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/skills/exploring-unfamiliar-code/SKILL.md +0 -0
  37. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/skills/exploring-unfamiliar-docs/SKILL.md +0 -0
  38. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/skills/pluck-chains/SKILL.md +0 -0
  39. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/skills/refactoring-with-squackit/SKILL.md +0 -0
  40. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/skills/using-squackit/SKILL.md +0 -0
  41. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/specs/2026-04-10-squawkit-design.md +0 -0
  42. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/specs/2026-04-12-cli-tool-exposure-design.md +0 -0
  43. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/specs/2026-04-12-pluckit-tool-integration-design.md +0 -0
  44. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/specs/2026-04-16-training-data-pipeline-design.md +0 -0
  45. {squackit-0.8.2 → squackit-0.8.3}/docs/superpowers/specs/2026-04-26-named-fts-collections-design.md +0 -0
  46. {squackit-0.8.2 → squackit-0.8.3}/docs/tools.md +0 -0
  47. {squackit-0.8.2 → squackit-0.8.3}/mkdocs.yml +0 -0
  48. {squackit-0.8.2 → squackit-0.8.3}/scripts/EXPERIMENT_LOG.md +0 -0
  49. {squackit-0.8.2 → squackit-0.8.3}/scripts/benchmark_models.py +0 -0
  50. {squackit-0.8.2 → squackit-0.8.3}/scripts/cascade_prototype.py +0 -0
  51. {squackit-0.8.2 → squackit-0.8.3}/scripts/combo_eval.py +0 -0
  52. {squackit-0.8.2 → squackit-0.8.3}/scripts/generate_examples.py +0 -0
  53. {squackit-0.8.2 → squackit-0.8.3}/scripts/grid_generate_examples.py +0 -0
  54. {squackit-0.8.2 → squackit-0.8.3}/scripts/hard_task_results/deepseek-coder-v2_16b.json +0 -0
  55. {squackit-0.8.2 → squackit-0.8.3}/scripts/hard_task_results/qwen2.5-coder_0.5b.json +0 -0
  56. {squackit-0.8.2 → squackit-0.8.3}/scripts/hard_task_results/qwen2.5-coder_3b.json +0 -0
  57. {squackit-0.8.2 → squackit-0.8.3}/scripts/hard_task_results/qwen2.5-coder_7b.json +0 -0
  58. {squackit-0.8.2 → squackit-0.8.3}/scripts/hard_task_results/qwen3.5_2b.json +0 -0
  59. {squackit-0.8.2 → squackit-0.8.3}/scripts/hard_task_results/qwen3_8b.json +0 -0
  60. {squackit-0.8.2 → squackit-0.8.3}/scripts/hard_task_results/scoring_input.json +0 -0
  61. {squackit-0.8.2 → squackit-0.8.3}/scripts/hard_task_results/summary.json +0 -0
  62. {squackit-0.8.2 → squackit-0.8.3}/scripts/hard_tasks.py +0 -0
  63. {squackit-0.8.2 → squackit-0.8.3}/scripts/harvest_grid.py +0 -0
  64. {squackit-0.8.2 → squackit-0.8.3}/scripts/iterative_eval.py +0 -0
  65. {squackit-0.8.2 → squackit-0.8.3}/scripts/iterative_results/summary.json +0 -0
  66. {squackit-0.8.2 → squackit-0.8.3}/scripts/iterative_results/summary_deepseek_plain.json +0 -0
  67. {squackit-0.8.2 → squackit-0.8.3}/scripts/iterative_results/summary_distilled_xml.json +0 -0
  68. {squackit-0.8.2 → squackit-0.8.3}/scripts/iterative_results/summary_editor_3b_7b.json +0 -0
  69. {squackit-0.8.2 → squackit-0.8.3}/scripts/iterative_results/summary_fulldocs.json +0 -0
  70. {squackit-0.8.2 → squackit-0.8.3}/scripts/iterative_results/summary_plaintext_3b7b.json +0 -0
  71. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/codegemma_2b.json +0 -0
  72. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/codegemma_7b.json +0 -0
  73. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/deepseek-coder-v2_16b.json +0 -0
  74. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/deepseek-llm_7b.json +0 -0
  75. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/gemma_latest.json +0 -0
  76. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/glm4_9b.json +0 -0
  77. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/granite-code_3b.json +0 -0
  78. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/granite3.1-dense_2b.json +0 -0
  79. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/granite3.3_2b.json +0 -0
  80. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/llama2-uncensored_7b.json +0 -0
  81. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/llama3.2-vision_11b.json +0 -0
  82. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/llama3.2_1b.json +0 -0
  83. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/llama3.2_latest.json +0 -0
  84. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/phi3_latest.json +0 -0
  85. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/phi4-mini_latest.json +0 -0
  86. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/qwen2.5-coder_0.5b.json +0 -0
  87. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/qwen2.5-coder_1.5b.json +0 -0
  88. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/qwen2.5-coder_3b.json +0 -0
  89. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/qwen2.5-coder_7b.json +0 -0
  90. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/qwen2.5_1.5b.json +0 -0
  91. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/qwen2.5_3b.json +0 -0
  92. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/qwen2.5_7b.json +0 -0
  93. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/qwen2_latest.json +0 -0
  94. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/qwen3.5_0.8b.json +0 -0
  95. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/qwen3.5_2b.json +0 -0
  96. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/qwen3.5_4b.json +0 -0
  97. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/qwen3.5_9b.json +0 -0
  98. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/qwen3_0.6b.json +0 -0
  99. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/qwen3_8b.json +0 -0
  100. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/smollm2_latest.json +0 -0
  101. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_card_results/summary.json +0 -0
  102. {squackit-0.8.2 → squackit-0.8.3}/scripts/model_cards.py +0 -0
  103. {squackit-0.8.2 → squackit-0.8.3}/scripts/multiline_results/deepseek-coder-v2_16b.json +0 -0
  104. {squackit-0.8.2 → squackit-0.8.3}/scripts/multiline_results/qwen2.5-coder_3b.json +0 -0
  105. {squackit-0.8.2 → squackit-0.8.3}/scripts/multiline_results/qwen2.5-coder_7b.json +0 -0
  106. {squackit-0.8.2 → squackit-0.8.3}/scripts/multiline_results/qwen3.5_2b.json +0 -0
  107. {squackit-0.8.2 → squackit-0.8.3}/scripts/multiline_results/qwen3_8b.json +0 -0
  108. {squackit-0.8.2 → squackit-0.8.3}/scripts/multiline_results/summary.json +0 -0
  109. {squackit-0.8.2 → squackit-0.8.3}/scripts/multiline_tasks.py +0 -0
  110. {squackit-0.8.2 → squackit-0.8.3}/scripts/qm_retrieval_eval.py +0 -0
  111. {squackit-0.8.2 → squackit-0.8.3}/scripts/quartermaster_eval.py +0 -0
  112. {squackit-0.8.2 → squackit-0.8.3}/scripts/quartermaster_results/retrieval_summary.json +0 -0
  113. {squackit-0.8.2 → squackit-0.8.3}/scripts/quartermaster_results/summary.json +0 -0
  114. {squackit-0.8.2 → squackit-0.8.3}/scripts/selector_eval.py +0 -0
  115. {squackit-0.8.2 → squackit-0.8.3}/scripts/selector_results/summary.json +0 -0
  116. {squackit-0.8.2 → squackit-0.8.3}/squackit/__main__.py +0 -0
  117. {squackit-0.8.2 → squackit-0.8.3}/squackit/cli.py +0 -0
  118. {squackit-0.8.2 → squackit-0.8.3}/squackit/data/examples.json +0 -0
  119. {squackit-0.8.2 → squackit-0.8.3}/squackit/db.py +0 -0
  120. {squackit-0.8.2 → squackit-0.8.3}/squackit/defaults.py +0 -0
  121. {squackit-0.8.2 → squackit-0.8.3}/squackit/formatting.py +0 -0
  122. {squackit-0.8.2 → squackit-0.8.3}/squackit/lackpy_integration.py +0 -0
  123. {squackit-0.8.2 → squackit-0.8.3}/squackit/prompts.py +0 -0
  124. {squackit-0.8.2 → squackit-0.8.3}/squackit/runtime.py +0 -0
  125. {squackit-0.8.2 → squackit-0.8.3}/squackit/server.py +0 -0
  126. {squackit-0.8.2 → squackit-0.8.3}/squackit/session.py +0 -0
  127. {squackit-0.8.2 → squackit-0.8.3}/tests/conftest.py +0 -0
  128. {squackit-0.8.2 → squackit-0.8.3}/tests/fixtures/has_call_filter.py +0 -0
  129. {squackit-0.8.2 → squackit-0.8.3}/tests/test_cli_tools.py +0 -0
  130. {squackit-0.8.2 → squackit-0.8.3}/tests/test_config_consumption.py +0 -0
  131. {squackit-0.8.2 → squackit-0.8.3}/tests/test_defaults.py +0 -0
  132. {squackit-0.8.2 → squackit-0.8.3}/tests/test_formatting_json.py +0 -0
  133. {squackit-0.8.2 → squackit-0.8.3}/tests/test_fts_root.py +0 -0
  134. {squackit-0.8.2 → squackit-0.8.3}/tests/test_prompts.py +0 -0
  135. {squackit-0.8.2 → squackit-0.8.3}/tests/test_resources.py +0 -0
  136. {squackit-0.8.2 → squackit-0.8.3}/tests/test_runtime_config.py +0 -0
  137. {squackit-0.8.2 → squackit-0.8.3}/tests/test_sandbox_anchoring.py +0 -0
  138. {squackit-0.8.2 → squackit-0.8.3}/tests/test_session.py +0 -0
  139. {squackit-0.8.2 → squackit-0.8.3}/tests/test_tool_config.py +0 -0
  140. {squackit-0.8.2 → squackit-0.8.3}/tests/test_tools.py +0 -0
  141. {squackit-0.8.2 → squackit-0.8.3}/tests/test_truncation.py +0 -0
  142. {squackit-0.8.2 → squackit-0.8.3}/tests/test_workflows.py +0 -0
@@ -1,5 +1,67 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.3
4
+
5
+ Three fixes with one theme: a failure must not be representable as an ordinary
6
+ result. Nothing here changes what a working call returns.
7
+
8
+ ### Fixed — `investigate` reported failed lookups as confirmed absences (#14)
9
+
10
+ Every exception from the definition query was caught and rendered with the
11
+ sentence used for a genuine miss:
12
+
13
+ No definition found for 'X'. Try a broader pattern or check spelling.
14
+
15
+ So a permission error, a binder error and "that symbol isn't here" were
16
+ indistinguishable, and the advice actively misdirected — the pattern was never
17
+ the problem. This is how the 0.8.1 sandbox bug presented: every symbol in a
18
+ healthy project reported missing, for hours, while the server called all its
19
+ tools healthy.
20
+
21
+ A failed lookup now says so, names the underlying error, and reports the
22
+ pattern it searched. The friendly wording is kept for the genuine empty case,
23
+ where it is good advice.
24
+
25
+ ### Fixed — a raising pluckin lost its tools silently (#14)
26
+
27
+ `collect_pluckin_tools` swallowed per-pluckin exceptions with a bare `pass`,
28
+ under a comment claiming the error would be "surfaced contextually". Nothing
29
+ surfaced it. The guard is right — one broken pluckin must not take down the
30
+ registry — but it now logs a warning naming the pluckin and the error.
31
+
32
+ Not reachable today (no shipped pluckin defines `squackit_tools`), fixed as
33
+ latent. The symptom it would produce — a silently shorter tool surface — was
34
+ independently hit through another mechanism and cost a four-combination version
35
+ bisection, because a shorter tool list doesn't read as a failure. It reads as a
36
+ different, reasonable version of the product.
37
+
38
+ ### Fixed — `ast_replace` / `ast_patch` read as mutating and are not (#16)
39
+
40
+ Connection macros are published with a description generated from the
41
+ signature, so these carried no semantics at all:
42
+
43
+ Query: ast_replace(source, selector, new_text, language)
44
+
45
+ These are sitting_duck SQL macros. A SQL macro cannot write files — it computes
46
+ the patched source and returns it, which is coherent, and sitting_duck is not
47
+ misbehaving. But over MCP the caller reads a verb. One did: it called
48
+ `ast_replace` to repair a bug, got a successful-looking table back, and changed
49
+ nothing on disk.
50
+
51
+ Generated descriptions for macros whose *name* implies an effect now state that
52
+ they return the patched result and modify nothing. Read-only macros are
53
+ unchanged — a disclaimer on all ninety is context every agent pays for.
54
+
55
+ For an actually-mutating primitive, see #17.
56
+
57
+ ### Fixed — `__version__` was four releases stale (#15, #2)
58
+
59
+ `squackit.__version__` was a hand-written `"0.4.1"` against a distribution
60
+ version of `0.8.2`, and survived `--force-reinstall` because it derived from
61
+ nothing. It is now read from `importlib.metadata`, and the smoke test asserts
62
+ that correspondence rather than a literal — which is what let both drift in
63
+ agreement with each other and with nothing else.
64
+
3
65
  ## 0.8.2
4
66
 
5
67
  ### Fixed — AST tools could not read a source outside the working directory
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: squackit
3
- Version: 0.8.2
3
+ Version: 0.8.3
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "squackit"
3
- version = "0.8.2"
3
+ version = "0.8.3"
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"
@@ -0,0 +1,17 @@
1
+ """squackit: Semi-QUalified Agent Companion Kit — the stateful intelligence + MCP server layer for fledgling-equipped agents."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version as _dist_version
4
+
5
+ # Derived, never written by hand. The literal that used to live here read
6
+ # "0.4.1" while the installed distribution was 0.8.2 — four releases stale, and
7
+ # immune to --force-reinstall because it was not derived from anything. Someone
8
+ # debugging an unrelated problem read it, concluded they had a mixed install,
9
+ # and went looking for a packaging fault that did not exist. A version string
10
+ # that lies makes a healthy install look broken, which is the most expensive
11
+ # direction to send a reader.
12
+ try:
13
+ __version__ = _dist_version("squackit")
14
+ except PackageNotFoundError: # running from a source tree with no install
15
+ __version__ = "0.0.0.dev0"
16
+
17
+ __all__ = ["__version__"]
@@ -37,6 +37,21 @@ _FALLBACK_NUMERIC = {
37
37
  }
38
38
 
39
39
 
40
+ #: Name tokens that read as an effect. A macro whose name contains one of these
41
+ #: gets an explicit "does not modify any file" note in its generated
42
+ #: description. Missing a verb costs nothing beyond the status quo; a false
43
+ #: positive adds a sentence that is true of every SQL macro anyway.
44
+ _MUTATION_VERBS = frozenset({
45
+ "replace", "patch", "apply", "write", "insert", "update", "delete",
46
+ "remove", "rename", "edit", "set", "modify", "save", "commit",
47
+ })
48
+
49
+
50
+ def _name_implies_mutation(name: str) -> bool:
51
+ """True when a tool's NAME reads as an effect it does not perform."""
52
+ return any(token in _MUTATION_VERBS for token in name.split("_"))
53
+
54
+
40
55
  @dataclass
41
56
  class ToolPresentation:
42
57
  """Wraps fledgling ToolInfo with squackit's presentation/UX config."""
@@ -100,7 +115,23 @@ class ToolPresentation:
100
115
  def description(self) -> str:
101
116
  if self.description_override is not None:
102
117
  return self.description_override
103
- return self.info.description or f"Query: {self.name}({', '.join(self.params)})"
118
+ if self.info.description:
119
+ return self.info.description
120
+ signature = f"{self.name}({', '.join(self.params)})"
121
+ if _name_implies_mutation(self.name):
122
+ # SQL macros cannot write files — they compute and return a result
123
+ # set. But a caller reads a verb: `ast_replace` was called to repair
124
+ # a bug, returned a successful-looking table, and changed nothing on
125
+ # disk. The bare signature said nothing either way, and the only
126
+ # hint was the "Query:" prefix — a category label competing against
127
+ # an imperative. Spell it out for the names where it misleads, and
128
+ # only those: a disclaimer on all ~90 macros is context every agent
129
+ # pays for on every call.
130
+ return (
131
+ f"Query: {signature} — computes and RETURNS the patched result "
132
+ f"as text. Does not modify any file."
133
+ )
134
+ return f"Query: {signature}"
104
135
 
105
136
  @property
106
137
  def parameters_schema(self) -> dict | None:
@@ -9,6 +9,7 @@ fledgling macro tools. They take priority over fledgling equivalents
9
9
  from __future__ import annotations
10
10
 
11
11
  import hashlib
12
+ import logging
12
13
  import os
13
14
  import re
14
15
  import tempfile
@@ -16,6 +17,8 @@ import tempfile
16
17
  from fledgling.tools import ToolInfo
17
18
  from squackit.tool_config import ToolPresentation
18
19
 
20
+ log = logging.getLogger(__name__)
21
+
19
22
 
20
23
  #: How much source text ``read_ast`` puts in the ``peek`` column when squackit
21
24
  #: materializes its own table. ``'smart'`` matches what pluckit's cache uses by
@@ -305,10 +308,20 @@ def collect_pluckin_tools(plucker) -> list:
305
308
  if callable(fn):
306
309
  try:
307
310
  tools.extend(fn())
308
- except Exception:
309
- # A broken pluckin shouldn't break the whole registry.
310
- # Squackit's server/CLI will surface the error contextually.
311
- pass
311
+ except Exception as exc:
312
+ # Keep the guard — one broken pluckin must not take down the
313
+ # whole registry — but SAY SO. Dropping its tools silently
314
+ # yields a shorter tool surface, and a shorter tool list does
315
+ # not read as a failure: it reads as a different, reasonable
316
+ # version of the product, so callers reason forward from it
317
+ # instead of doubting it. (An earlier comment here claimed the
318
+ # server/CLI would "surface the error contextually". Nothing
319
+ # did.)
320
+ log.warning(
321
+ "pluckin %s failed to contribute squackit tools (%s); its "
322
+ "tools are missing from this session's registry",
323
+ type(pluckin).__name__, exc, exc_info=True,
324
+ )
312
325
  return tools
313
326
 
314
327
 
@@ -160,13 +160,22 @@ def investigate(con, defaults, name, file_pattern=None, path=None):
160
160
  # the whole briefing describe the wrong symbol. Fall back to the full
161
161
  # substring set only when nothing matches exactly, preserving the
162
162
  # forgiving lookup for partial names / typos.
163
+ # A FAILED LOOKUP IS NOT AN ABSENT SYMBOL. Both used to return the "no
164
+ # definition found" sentence below, which made a permission error, a binder
165
+ # error and a genuine miss indistinguishable — and told the reader to
166
+ # broaden a pattern that was never the problem. That is exactly how the
167
+ # 0.8.1 sandbox bug presented: every symbol in a healthy project reported
168
+ # missing, for hours, while the server called all its tools healthy.
169
+ lookup_error: Exception | None = None
163
170
  try:
164
171
  rel = con.find_definitions(
165
172
  file_pattern=file_pattern, name_pattern=f"%{name}%",
166
173
  )
167
174
  defs = rel.fetchall()
168
175
  def_cols = rel.columns
169
- except Exception:
176
+ except Exception as exc:
177
+ log.debug("find_definitions failed for %r", name, exc_info=True)
178
+ lookup_error = exc
170
179
  defs = []
171
180
  def_cols = []
172
181
 
@@ -176,6 +185,14 @@ def investigate(con, defaults, name, file_pattern=None, path=None):
176
185
  if _exact:
177
186
  defs = _exact
178
187
 
188
+ if lookup_error is not None:
189
+ return (
190
+ f"Could not look up '{name}' — the definition query failed, so "
191
+ f"whether this symbol exists is UNKNOWN.\n"
192
+ f"{type(lookup_error).__name__}: {lookup_error}\n"
193
+ f"Searched: {file_pattern}"
194
+ )
195
+
179
196
  if not defs:
180
197
  return f"No definition found for '{name}'. Try a broader pattern or check spelling."
181
198
 
@@ -0,0 +1,198 @@
1
+ """A failure must not be representable as an ordinary result.
2
+
3
+ Three places used to violate that, each in a different way:
4
+
5
+ * ``investigate`` caught every query exception and rendered it with the same
6
+ sentence it uses for a genuine miss — so a permission error, a binder error
7
+ and "that symbol isn't here" were indistinguishable, and the advice offered
8
+ ("try a broader pattern") actively misdirected. This is not hypothetical: it
9
+ is exactly how the 0.8.1 sandbox bug presented, for hours, while the server
10
+ reported every tool healthy.
11
+ * ``collect_pluckin_tools`` dropped a raising pluckin's tools with a bare
12
+ ``pass`` under a comment claiming the error would be surfaced elsewhere.
13
+ Nothing surfaced it. A silently shorter tool surface is worse than a silently
14
+ empty result — an empty result prompts "did that work?", a shorter tool list
15
+ reads as a different, reasonable version of the product.
16
+ * Auto-generated macro descriptions were a bare signature, so a SQL macro named
17
+ ``ast_replace`` — which computes patched source and writes nothing, because a
18
+ SQL macro *cannot* write — read as a mutation to any caller.
19
+
20
+ See issues #14 and #16.
21
+ """
22
+
23
+ import logging
24
+
25
+ import pytest
26
+
27
+ from squackit.defaults import ProjectDefaults
28
+ from squackit.tool_config import ToolPresentation
29
+ from squackit.tools import collect_pluckin_tools
30
+ from squackit.workflows import investigate
31
+
32
+
33
+ # ── investigate: a failed query is not a miss ───────────────────────
34
+
35
+
36
+ class _Rel:
37
+ def __init__(self, rows, cols):
38
+ self._rows, self.columns = rows, cols
39
+
40
+ def fetchall(self):
41
+ return self._rows
42
+
43
+
44
+ class _ConRaises:
45
+ """A connection whose lookup fails the way a sandboxed one does."""
46
+
47
+ def __init__(self, exc):
48
+ self._exc = exc
49
+
50
+ def find_definitions(self, **kw):
51
+ raise self._exc
52
+
53
+
54
+ class _ConEmpty:
55
+ """A connection that works and genuinely has no match."""
56
+
57
+ def find_definitions(self, **kw):
58
+ return _Rel([], ["file_path", "name", "kind",
59
+ "start_line", "end_line", "signature"])
60
+
61
+
62
+ @pytest.fixture
63
+ def defaults(tmp_path):
64
+ return ProjectDefaults(code_pattern="**/*.py", root=str(tmp_path))
65
+
66
+
67
+ class TestInvestigateDistinguishesFailureFromMiss:
68
+
69
+ def test_genuine_miss_keeps_the_helpful_message(self, defaults):
70
+ """The friendly wording is right when it IS a miss — keep it."""
71
+ out = investigate(_ConEmpty(), defaults, "nonexistent_symbol")
72
+ assert "No definition found" in out
73
+ assert "nonexistent_symbol" in out
74
+
75
+ def test_query_failure_does_NOT_claim_the_symbol_is_absent(self, defaults):
76
+ out = investigate(_ConRaises(RuntimeError("boom")), defaults, "parse_config")
77
+ assert "No definition found" not in out, (
78
+ "a failed lookup was reported as a confirmed absence"
79
+ )
80
+
81
+ def test_query_failure_says_it_failed(self, defaults):
82
+ out = investigate(_ConRaises(RuntimeError("boom")), defaults, "parse_config")
83
+ assert "could not" in out.lower() or "failed" in out.lower()
84
+
85
+ def test_query_failure_surfaces_the_underlying_error(self, defaults):
86
+ """The 0.8.1 bug was diagnosable only from this text."""
87
+ exc = RuntimeError(
88
+ 'Permission Error: Cannot access file "**/*.py" '
89
+ "- file system operations are disabled by configuration"
90
+ )
91
+ out = investigate(_ConRaises(exc), defaults, "parse_config")
92
+ assert "Permission Error" in out, out
93
+
94
+ def test_query_failure_does_NOT_advise_broadening_the_pattern(self, defaults):
95
+ """That advice sends the reader at the wrong thing entirely."""
96
+ out = investigate(_ConRaises(RuntimeError("boom")), defaults, "parse_config")
97
+ assert "broader pattern" not in out
98
+
99
+
100
+ # ── collect_pluckin_tools: a dropped pluckin is logged ───────────────
101
+
102
+
103
+ class _Pluckin:
104
+ def __init__(self, tools=None, exc=None):
105
+ self._tools, self._exc = tools or [], exc
106
+
107
+ def squackit_tools(self):
108
+ if self._exc:
109
+ raise self._exc
110
+ return self._tools
111
+
112
+
113
+ class _Plucker:
114
+ def __init__(self, pluckins):
115
+ self.pluckins = pluckins
116
+
117
+
118
+ class TestPluckinFailuresAreVisible:
119
+
120
+ def test_a_raising_pluckin_does_not_break_the_registry(self):
121
+ """The guard itself is correct and stays."""
122
+ good = _Pluckin(tools=["t1", "t2"])
123
+ bad = _Pluckin(exc=RuntimeError("plugin exploded"))
124
+ assert collect_pluckin_tools(_Plucker([bad, good])) == ["t1", "t2"]
125
+
126
+ def test_a_raising_pluckin_is_LOGGED(self, caplog):
127
+ bad = _Pluckin(exc=RuntimeError("plugin exploded"))
128
+ with caplog.at_level(logging.WARNING, logger="squackit.tools"):
129
+ collect_pluckin_tools(_Plucker([bad]))
130
+ assert caplog.records, "a pluckin's tools vanished with no log record"
131
+
132
+ def test_the_log_names_the_pluckin_and_the_error(self, caplog):
133
+ bad = _Pluckin(exc=RuntimeError("plugin exploded"))
134
+ with caplog.at_level(logging.WARNING, logger="squackit.tools"):
135
+ collect_pluckin_tools(_Plucker([bad]))
136
+ text = " ".join(r.getMessage() for r in caplog.records)
137
+ assert "_Pluckin" in text, text
138
+ assert "plugin exploded" in text, text
139
+
140
+ def test_healthy_pluckins_log_nothing(self, caplog):
141
+ with caplog.at_level(logging.WARNING, logger="squackit.tools"):
142
+ collect_pluckin_tools(_Plucker([_Pluckin(tools=["t1"])]))
143
+ assert not caplog.records
144
+
145
+
146
+ # ── generated descriptions state their semantics ─────────────────────
147
+
148
+
149
+ class _Info:
150
+ """Stand-in for fledgling's ToolInfo carrying no description of its own.
151
+
152
+ ToolPresentation derives `name` and `params` from the info object, so the
153
+ stub has to supply macro_name/params rather than the presentation taking
154
+ them directly.
155
+ """
156
+
157
+ def __init__(self, macro_name, params, description=None):
158
+ self.macro_name = macro_name
159
+ self.tool_name = None
160
+ self.params = list(params)
161
+ self.description = description
162
+ self.required = None
163
+ self.required_params = []
164
+ self.format = None
165
+ self.parameters_schema = None
166
+
167
+
168
+ def _presentation(name, params):
169
+ return ToolPresentation(info=_Info(name, params))
170
+
171
+
172
+ class TestMutationNamedMacrosSayTheyDoNotMutate:
173
+
174
+ def test_ast_replace_states_it_does_not_modify(self):
175
+ d = _presentation("ast_replace", ["source", "selector", "new_text"])
176
+ assert "does not modify" in d.description.lower(), d.description
177
+
178
+ def test_ast_patch_states_it_does_not_modify(self):
179
+ d = _presentation("ast_patch", ["edits", "files"])
180
+ assert "does not modify" in d.description.lower(), d.description
181
+
182
+ def test_the_signature_is_still_there(self):
183
+ d = _presentation("ast_replace", ["source", "selector", "new_text"])
184
+ assert "ast_replace(source, selector, new_text)" in d.description
185
+
186
+ def test_read_only_macros_are_NOT_cluttered(self):
187
+ """A disclaimer on all 90 macros is context every agent pays for."""
188
+ d = _presentation("find_definitions", ["file_pattern", "name_pattern"])
189
+ assert "does not modify" not in d.description.lower()
190
+
191
+ def test_a_real_description_still_wins(self):
192
+ info = _Info("ast_replace", ["source"], description="The macro's own words.")
193
+ assert ToolPresentation(info=info).description == "The macro's own words."
194
+
195
+ def test_an_override_still_wins(self):
196
+ d = ToolPresentation(info=_Info("ast_replace", ["source"]),
197
+ description_override="Overridden.")
198
+ assert d.description == "Overridden."
@@ -3,7 +3,23 @@
3
3
 
4
4
  def test_import_squackit():
5
5
  import squackit
6
- assert squackit.__version__ == "0.4.1"
6
+ assert squackit.__version__
7
+
8
+
9
+ def test_version_matches_the_installed_distribution():
10
+ """Pin the exported version to the distribution, not to a literal.
11
+
12
+ Asserting a hardcoded string here is what let `__version__` drift four
13
+ releases behind pyproject.toml: bumping the release left the module and
14
+ this test agreeing with each other and with nothing else. See #2 and #15.
15
+ """
16
+ from importlib.metadata import PackageNotFoundError, version
17
+ import squackit
18
+ try:
19
+ expected = version("squackit")
20
+ except PackageNotFoundError:
21
+ expected = "0.0.0.dev0"
22
+ assert squackit.__version__ == expected
7
23
 
8
24
 
9
25
  def test_fledgling_available():
@@ -1,3 +0,0 @@
1
- """squackit: Semi-QUalified Agent Companion Kit — the stateful intelligence + MCP server layer for fledgling-equipped agents."""
2
-
3
- __version__ = "0.4.1"
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