squackit 0.5.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.
Files changed (138) hide show
  1. squackit-0.7.0/CHANGELOG.md +56 -0
  2. {squackit-0.5.0 → squackit-0.7.0}/PKG-INFO +5 -3
  3. {squackit-0.5.0 → squackit-0.7.0}/README.md +4 -2
  4. squackit-0.7.0/docs/proposals/2026-06-04-response-type-improvements.md +134 -0
  5. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/skills/exploring-unfamiliar-code/SKILL.md +12 -2
  6. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/skills/pluck-chains/SKILL.md +26 -8
  7. {squackit-0.5.0 → squackit-0.7.0}/pyproject.toml +1 -1
  8. {squackit-0.5.0 → squackit-0.7.0}/scripts/iterative_eval.py +1 -1
  9. {squackit-0.5.0 → squackit-0.7.0}/squackit/lackpy_integration.py +2 -2
  10. squackit-0.7.0/squackit/runtime.py +167 -0
  11. {squackit-0.5.0 → squackit-0.7.0}/squackit/server.py +166 -6
  12. {squackit-0.5.0 → squackit-0.7.0}/squackit/tool_config.py +7 -0
  13. {squackit-0.5.0 → squackit-0.7.0}/squackit/tools.py +7 -1
  14. {squackit-0.5.0 → squackit-0.7.0}/squackit/workflows.py +90 -14
  15. squackit-0.7.0/tests/test_fts_root.py +60 -0
  16. squackit-0.7.0/tests/test_runtime_config.py +175 -0
  17. {squackit-0.5.0 → squackit-0.7.0}/tests/test_workflows.py +45 -0
  18. squackit-0.5.0/CHANGELOG.md +0 -27
  19. {squackit-0.5.0 → squackit-0.7.0}/.github/workflows/release.yml +0 -0
  20. {squackit-0.5.0 → squackit-0.7.0}/.gitignore +0 -0
  21. {squackit-0.5.0 → squackit-0.7.0}/.mcp.json +0 -0
  22. {squackit-0.5.0 → squackit-0.7.0}/.readthedocs.yaml +0 -0
  23. {squackit-0.5.0 → squackit-0.7.0}/.readthedocs.yml +0 -0
  24. {squackit-0.5.0 → squackit-0.7.0}/CLAUDE.md +0 -0
  25. {squackit-0.5.0 → squackit-0.7.0}/LICENSE +0 -0
  26. {squackit-0.5.0 → squackit-0.7.0}/docs/agent-system-prompt.md +0 -0
  27. {squackit-0.5.0 → squackit-0.7.0}/docs/architecture.md +0 -0
  28. {squackit-0.5.0 → squackit-0.7.0}/docs/configuration.md +0 -0
  29. {squackit-0.5.0 → squackit-0.7.0}/docs/index.md +0 -0
  30. {squackit-0.5.0 → squackit-0.7.0}/docs/prompts.md +0 -0
  31. {squackit-0.5.0 → squackit-0.7.0}/docs/quickstart.md +0 -0
  32. {squackit-0.5.0 → squackit-0.7.0}/docs/resources.md +0 -0
  33. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-10-phase1-handoff.md +0 -0
  34. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-10-squawkit-extraction.md +0 -0
  35. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-11-phase3-pluckit-rewire.md +0 -0
  36. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-12-cli-tool-exposure.md +0 -0
  37. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-12-pluckit-tool-integration.md +0 -0
  38. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-12-session-handoff.md +0 -0
  39. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-16-session-handoff.md +0 -0
  40. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-26-named-fts-collections.md +0 -0
  41. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/plans/2026-04-27-kibitzer-fts-doc-retrieval.md +0 -0
  42. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/skills/exploring-unfamiliar-docs/SKILL.md +0 -0
  43. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/skills/refactoring-with-squackit/SKILL.md +0 -0
  44. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/skills/using-squackit/SKILL.md +0 -0
  45. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/specs/2026-04-10-squawkit-design.md +0 -0
  46. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/specs/2026-04-12-cli-tool-exposure-design.md +0 -0
  47. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/specs/2026-04-12-pluckit-tool-integration-design.md +0 -0
  48. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/specs/2026-04-16-training-data-pipeline-design.md +0 -0
  49. {squackit-0.5.0 → squackit-0.7.0}/docs/superpowers/specs/2026-04-26-named-fts-collections-design.md +0 -0
  50. {squackit-0.5.0 → squackit-0.7.0}/docs/tools.md +0 -0
  51. {squackit-0.5.0 → squackit-0.7.0}/mkdocs.yml +0 -0
  52. {squackit-0.5.0 → squackit-0.7.0}/scripts/EXPERIMENT_LOG.md +0 -0
  53. {squackit-0.5.0 → squackit-0.7.0}/scripts/benchmark_models.py +0 -0
  54. {squackit-0.5.0 → squackit-0.7.0}/scripts/cascade_prototype.py +0 -0
  55. {squackit-0.5.0 → squackit-0.7.0}/scripts/combo_eval.py +0 -0
  56. {squackit-0.5.0 → squackit-0.7.0}/scripts/generate_examples.py +0 -0
  57. {squackit-0.5.0 → squackit-0.7.0}/scripts/grid_generate_examples.py +0 -0
  58. {squackit-0.5.0 → squackit-0.7.0}/scripts/hard_task_results/deepseek-coder-v2_16b.json +0 -0
  59. {squackit-0.5.0 → squackit-0.7.0}/scripts/hard_task_results/qwen2.5-coder_0.5b.json +0 -0
  60. {squackit-0.5.0 → squackit-0.7.0}/scripts/hard_task_results/qwen2.5-coder_3b.json +0 -0
  61. {squackit-0.5.0 → squackit-0.7.0}/scripts/hard_task_results/qwen2.5-coder_7b.json +0 -0
  62. {squackit-0.5.0 → squackit-0.7.0}/scripts/hard_task_results/qwen3.5_2b.json +0 -0
  63. {squackit-0.5.0 → squackit-0.7.0}/scripts/hard_task_results/qwen3_8b.json +0 -0
  64. {squackit-0.5.0 → squackit-0.7.0}/scripts/hard_task_results/scoring_input.json +0 -0
  65. {squackit-0.5.0 → squackit-0.7.0}/scripts/hard_task_results/summary.json +0 -0
  66. {squackit-0.5.0 → squackit-0.7.0}/scripts/hard_tasks.py +0 -0
  67. {squackit-0.5.0 → squackit-0.7.0}/scripts/harvest_grid.py +0 -0
  68. {squackit-0.5.0 → squackit-0.7.0}/scripts/iterative_results/summary.json +0 -0
  69. {squackit-0.5.0 → squackit-0.7.0}/scripts/iterative_results/summary_deepseek_plain.json +0 -0
  70. {squackit-0.5.0 → squackit-0.7.0}/scripts/iterative_results/summary_distilled_xml.json +0 -0
  71. {squackit-0.5.0 → squackit-0.7.0}/scripts/iterative_results/summary_editor_3b_7b.json +0 -0
  72. {squackit-0.5.0 → squackit-0.7.0}/scripts/iterative_results/summary_fulldocs.json +0 -0
  73. {squackit-0.5.0 → squackit-0.7.0}/scripts/iterative_results/summary_plaintext_3b7b.json +0 -0
  74. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/codegemma_2b.json +0 -0
  75. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/codegemma_7b.json +0 -0
  76. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/deepseek-coder-v2_16b.json +0 -0
  77. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/deepseek-llm_7b.json +0 -0
  78. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/gemma_latest.json +0 -0
  79. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/glm4_9b.json +0 -0
  80. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/granite-code_3b.json +0 -0
  81. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/granite3.1-dense_2b.json +0 -0
  82. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/granite3.3_2b.json +0 -0
  83. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/llama2-uncensored_7b.json +0 -0
  84. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/llama3.2-vision_11b.json +0 -0
  85. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/llama3.2_1b.json +0 -0
  86. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/llama3.2_latest.json +0 -0
  87. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/phi3_latest.json +0 -0
  88. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/phi4-mini_latest.json +0 -0
  89. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/qwen2.5-coder_0.5b.json +0 -0
  90. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/qwen2.5-coder_1.5b.json +0 -0
  91. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/qwen2.5-coder_3b.json +0 -0
  92. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/qwen2.5-coder_7b.json +0 -0
  93. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/qwen2.5_1.5b.json +0 -0
  94. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/qwen2.5_3b.json +0 -0
  95. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/qwen2.5_7b.json +0 -0
  96. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/qwen2_latest.json +0 -0
  97. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/qwen3.5_0.8b.json +0 -0
  98. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/qwen3.5_2b.json +0 -0
  99. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/qwen3.5_4b.json +0 -0
  100. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/qwen3.5_9b.json +0 -0
  101. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/qwen3_0.6b.json +0 -0
  102. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/qwen3_8b.json +0 -0
  103. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/smollm2_latest.json +0 -0
  104. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_card_results/summary.json +0 -0
  105. {squackit-0.5.0 → squackit-0.7.0}/scripts/model_cards.py +0 -0
  106. {squackit-0.5.0 → squackit-0.7.0}/scripts/multiline_results/deepseek-coder-v2_16b.json +0 -0
  107. {squackit-0.5.0 → squackit-0.7.0}/scripts/multiline_results/qwen2.5-coder_3b.json +0 -0
  108. {squackit-0.5.0 → squackit-0.7.0}/scripts/multiline_results/qwen2.5-coder_7b.json +0 -0
  109. {squackit-0.5.0 → squackit-0.7.0}/scripts/multiline_results/qwen3.5_2b.json +0 -0
  110. {squackit-0.5.0 → squackit-0.7.0}/scripts/multiline_results/qwen3_8b.json +0 -0
  111. {squackit-0.5.0 → squackit-0.7.0}/scripts/multiline_results/summary.json +0 -0
  112. {squackit-0.5.0 → squackit-0.7.0}/scripts/multiline_tasks.py +0 -0
  113. {squackit-0.5.0 → squackit-0.7.0}/scripts/qm_retrieval_eval.py +0 -0
  114. {squackit-0.5.0 → squackit-0.7.0}/scripts/quartermaster_eval.py +0 -0
  115. {squackit-0.5.0 → squackit-0.7.0}/scripts/quartermaster_results/retrieval_summary.json +0 -0
  116. {squackit-0.5.0 → squackit-0.7.0}/scripts/quartermaster_results/summary.json +0 -0
  117. {squackit-0.5.0 → squackit-0.7.0}/scripts/selector_eval.py +0 -0
  118. {squackit-0.5.0 → squackit-0.7.0}/scripts/selector_results/summary.json +0 -0
  119. {squackit-0.5.0 → squackit-0.7.0}/squackit/__init__.py +0 -0
  120. {squackit-0.5.0 → squackit-0.7.0}/squackit/__main__.py +0 -0
  121. {squackit-0.5.0 → squackit-0.7.0}/squackit/cli.py +0 -0
  122. {squackit-0.5.0 → squackit-0.7.0}/squackit/data/examples.json +0 -0
  123. {squackit-0.5.0 → squackit-0.7.0}/squackit/db.py +0 -0
  124. {squackit-0.5.0 → squackit-0.7.0}/squackit/defaults.py +0 -0
  125. {squackit-0.5.0 → squackit-0.7.0}/squackit/formatting.py +0 -0
  126. {squackit-0.5.0 → squackit-0.7.0}/squackit/prompts.py +0 -0
  127. {squackit-0.5.0 → squackit-0.7.0}/squackit/session.py +0 -0
  128. {squackit-0.5.0 → squackit-0.7.0}/tests/conftest.py +0 -0
  129. {squackit-0.5.0 → squackit-0.7.0}/tests/test_cli_tools.py +0 -0
  130. {squackit-0.5.0 → squackit-0.7.0}/tests/test_defaults.py +0 -0
  131. {squackit-0.5.0 → squackit-0.7.0}/tests/test_formatting_json.py +0 -0
  132. {squackit-0.5.0 → squackit-0.7.0}/tests/test_prompts.py +0 -0
  133. {squackit-0.5.0 → squackit-0.7.0}/tests/test_resources.py +0 -0
  134. {squackit-0.5.0 → squackit-0.7.0}/tests/test_session.py +0 -0
  135. {squackit-0.5.0 → squackit-0.7.0}/tests/test_smoke.py +0 -0
  136. {squackit-0.5.0 → squackit-0.7.0}/tests/test_tool_config.py +0 -0
  137. {squackit-0.5.0 → squackit-0.7.0}/tests/test_tools.py +0 -0
  138. {squackit-0.5.0 → squackit-0.7.0}/tests/test_truncation.py +0 -0
@@ -0,0 +1,56 @@
1
+ # Changelog
2
+
3
+ ## 0.6.0
4
+
5
+ ### Added — `investigate(path=)` for cross-project scoping
6
+ `investigate(name, path=)` accepts an explicit `path` argument and scopes the
7
+ symbol-lookup to that path's `scoped_code_pattern`. Without `path=`, defaults
8
+ to the process `cwd`-scoped pattern (was: the global registry pattern, which
9
+ substring-matched across every indexed project — e.g. `investigate("main")`
10
+ returned hits from vendored JS in unrelated repos). Pre-existing
11
+ `file_pattern=` overrides still work.
12
+
13
+ ### Added — per-root FTS search
14
+ `search_*` tools now accept `root=<dir>` to index + search any repo on the
15
+ fly. Indexes are LRU-cached per root for the session, so repeated queries
16
+ in the same root stay cheap.
17
+
18
+ ### Fixed — agent-ergonomics findings
19
+ - FTS now fails loud on index errors instead of silently returning empty.
20
+ - `investigate` no longer mislabels callers in cross-file results.
21
+ - Selector docs verified end-to-end against the running tool surface.
22
+ - `lackpy` 0.12 dropped top-level re-exports; switched to submodule imports
23
+ to keep import-time light.
24
+
25
+ ### Docs
26
+ - `:has(.call#NAME)` works now that pluckit delegates selector compilation
27
+ to sitting_duck (was a notable known-bug in 0.5.0).
28
+ - Tool count corrected in README (~20 was wrong; actually ~35 / ~40 full).
29
+ - Receiver-qualified call selectors (#4) marked DONE.
30
+ - Response-type improvement proposals collected in `docs/proposals/`.
31
+
32
+ ## 0.5.0
33
+
34
+ ### Changed — migrate off pluckit/fledgling private internals
35
+ squackit no longer reaches into private connection attributes; it now uses the
36
+ public, SemVer'd contract from fledgling 0.10 and pluckit 0.13:
37
+
38
+ - `con._con` → `con.con`; `con._tools` → `con.tools` (`server.py`, `cli.py`).
39
+ - The lazy FTS rebuild (which poked `con._con` + a private `_fts_built` flag)
40
+ now delegates to fledgling's `Connection.ensure_fts()`. squackit keeps only
41
+ the `_FTS_MACROS` gating (which tools need FTS).
42
+ - `plucker._registry.pluckins` → `plucker.pluckins`; `Chain._MUTATION_OPS`
43
+ → `Chain.MUTATION_OPS` (`tools.py`).
44
+
45
+ ### Dependencies — declare what we actually use, with compatible ranges
46
+ - `ast-pluckit>=0.13.0,<0.14` (was `>=0.9.0`) — needs the public `pluckins`
47
+ accessor + `pluckins.search`/`viewer`.
48
+ - `fledgling-mcp>=0.10.0,<0.11` — **newly declared.** squackit imports
49
+ `fledgling.tools.ToolInfo` and needs the `Connection.con/.tools/.ensure_fts`
50
+ contract; declaring it makes `Plucker.connection` always a `fledgling.Connection`
51
+ (ending the bare-duckdb surprise that caused the `'_con' missing` failures).
52
+
53
+ ### Net
54
+ The brittle, undocumented, conditionally-present coupling is gone: a grep for
55
+ private reach-ins (`con._con`, `con._tools`, `_registry`, `_MUTATION_OPS`,
56
+ `_fts_built`) returns zero. The suite now composes by a versioned public API.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: squackit
3
- Version: 0.5.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
@@ -71,8 +71,10 @@ Tab completion is built in (`eval "$(_SQUACKIT_COMPLETE=bash_source squackit)"`)
71
71
 
72
72
  ## What agents get via MCP
73
73
 
74
- - **20 tools** — CSS-selector code queries (via pluckit), file I/O, git history,
75
- doc navigation, diagnostics
74
+ - **~35 tools** — CSS-selector code queries (via pluckit), file I/O, git history,
75
+ doc navigation, conversation analytics, and diagnostics. The exact set is dynamic
76
+ (fledgling macros + pluckit tools + pluckins); a full install exposes ~40 MCP tools
77
+ including the workflows below.
76
78
  - **4 compound workflows** — `explore`, `investigate`, `review`, `search`
77
79
  - **3 prompt templates** — pre-loaded with live project data
78
80
  - **5 resources** — project overview, docs, git status, session log, diagnostics
@@ -42,8 +42,10 @@ Tab completion is built in (`eval "$(_SQUACKIT_COMPLETE=bash_source squackit)"`)
42
42
 
43
43
  ## What agents get via MCP
44
44
 
45
- - **20 tools** — CSS-selector code queries (via pluckit), file I/O, git history,
46
- doc navigation, diagnostics
45
+ - **~35 tools** — CSS-selector code queries (via pluckit), file I/O, git history,
46
+ doc navigation, conversation analytics, and diagnostics. The exact set is dynamic
47
+ (fledgling macros + pluckit tools + pluckins); a full install exposes ~40 MCP tools
48
+ including the workflows below.
47
49
  - **4 compound workflows** — `explore`, `investigate`, `review`, `search`
48
50
  - **3 prompt templates** — pre-loaded with live project data
49
51
  - **5 resources** — project overview, docs, git status, session log, diagnostics
@@ -0,0 +1,134 @@
1
+ # Response-type improvements — surfaced from dogfooding
2
+
3
+ Empirical findings from an exploration session against `~/Projects/blq` on 2026-06-04. The skill descriptions accurately steered me to the right verbs (`explore` → `find_names` → `investigate` → `find`), but several response-shape issues forced workarounds I wouldn't have needed. Each item below is a discrete change; cherry-pick what's worth doing.
4
+
5
+ ## 1. `explore` — partial failures masquerade as success
6
+
7
+ **Symptom**
8
+
9
+ ```
10
+ ### Key Definitions (top 20 by complexity)
11
+ (could not load)
12
+ ```
13
+
14
+ The overall call returned `{"result": "..."}` — success — but the most useful section silently degraded. No reason given, no signal in the response shape that anything went wrong. A caller programming against this can't distinguish "no definitions" from "extraction failed."
15
+
16
+ **Proposal**
17
+
18
+ Either of:
19
+
20
+ - **(a) Structured result** — switch from one giant markdown string to `{languages: ..., key_definitions: {status, data, reason?}, docs: ..., recent_activity: {status, data, reason?}}`. Each section reports its own status. A failed section still has a `reason` field ("FTS index not populated for /path", "AST extraction unsupported for language X", etc.).
21
+ - **(b) Inline error markers** — keep markdown but replace `(could not load)` with `(error: FTS index not populated — run \`fts_stats()\` to check, then rebuild)`. Same string format, just useful enough to act on.
22
+
23
+ (a) is the bigger improvement but (b) is one-line. Maybe both: (a) for the API, (b) as a fallback for the rendered output.
24
+
25
+ ## 2. `explore` — `recent_activity` ignores the `path` argument
26
+
27
+ **Symptom**
28
+
29
+ ```python
30
+ explore(path="/home/teague/Projects/blq")
31
+ # Languages, Documentation sections correctly reflect /Projects/blq
32
+ # Recent Activity shows commits from /home/teague/.dotfiles (session cwd)
33
+ ```
34
+
35
+ The git-log subquery in `recent_activity` runs against cwd, not against the requested `path`. Language scan and doc scan both honor `path`; only this one section leaks.
36
+
37
+ **Proposal**
38
+
39
+ Pipe `path` through to the git-context resolver in `explore`'s `recent_activity` builder, same way it reaches `project_overview`'s language scan. If `path` isn't a git repo, return an empty `recent_activity` with `reason="not a git repository"` rather than silently falling back to cwd.
40
+
41
+ ## 3. `investigate` — Source section truncates without signaling
42
+
43
+ **Symptom**
44
+
45
+ ```
46
+ ### Definition
47
+ | ... | start_line | end_line | ... |
48
+ | ... | 187 | 242 | ... |
49
+
50
+ ### Source
51
+ 187 def _apply_mcp_thread_limits() -> None:
52
+ ...
53
+ 236 ThreadPoolExecutor(max_workers=async_workers, ...),
54
+ ```
55
+
56
+ Definition says the function ends at line 242. Source stops at 236. No "...truncated" footer, no `max_lines` parameter in the call to bump. The caller has to notice the line-number mismatch to know there's missing source.
57
+
58
+ **Proposal**
59
+
60
+ Either:
61
+
62
+ - Always render Source through line `end_line` (the def is bounded; serving the full body is the point of `investigate`).
63
+ - OR add a `max_source_lines` parameter (default high enough to fit most functions) plus a footer `[truncated — N more lines; fetch with read_source(file_path, lines="237-242")]` when capped.
64
+
65
+ ## 4. Selectors match call names, not qualified attribute paths — DONE (sitting_duck c458a11)
66
+
67
+ **Symptom**
68
+
69
+ ```python
70
+ find(source="/blq/**/*.py", selector=".call#connect")
71
+ # Returns 28 rows: 21 duckdb.connect(...), 4 db.connect(), 3 bare connect()
72
+ ```
73
+
74
+ `.call#connect` matches any call whose attribute name is `connect`, regardless of receiver. To ask "every call to `duckdb.connect`", I had to inspect `peek` post-hoc.
75
+
76
+ **Implemented**
77
+
78
+ New `[receiver=X]` attribute filter on the existing `[attr op value]` selector surface. Supports `=`, `*=`, `^=`, `$=`. Examples:
79
+
80
+ ```python
81
+ find(source="/blq/**/*.py", selector=".call#connect[receiver=duckdb]")
82
+ # 21 rows — only duckdb.connect(...)
83
+
84
+ find(source="/blq/**/*.py", selector=".call#connect[receiver=db]")
85
+ # 4 rows — only db.connect()
86
+
87
+ find(source="/blq/**/*.py", selector=".call#connect[receiver$=connect]")
88
+ # Calls whose receiver chain ENDS with "connect" (rare but expressive)
89
+ ```
90
+
91
+ Chose attribute syntax over `.call#duckdb.connect` because the latter isn't valid CSS (`#` can only appear once; `.` separates classes). Receivers containing parens (`(complex).method()`) and computed receivers (`getattr(...)()`) are out of scope for the first cut.
92
+
93
+ Requires sitting_duck rebuild for the change to land in production. Tests at `sitting_duck/test/sql/ast_select_native.test` (4 cases covering positive/negative + 3 operators).
94
+
95
+ ## 5. `find` returns ~15 columns; ~4 are useful
96
+
97
+ **Symptom**
98
+
99
+ A `find` row currently includes: `node_id, type, semantic_type, flags, name, qualified_name, signature_type, parameters, modifiers, annotations, file_path, language, start_line, end_line, parent_id, depth, sibling_index, children_count, descendant_count, scope, peek`.
100
+
101
+ For 99% of caller intents, only `file_path`, `start_line`, `end_line`, `name`, and `peek` matter. The rest is internal AST bookkeeping that bloats responses (and Claude's context window).
102
+
103
+ **Proposal**
104
+
105
+ Default to a compact column set — `file_path | start_line | end_line | name | peek`. Add `verbose=True` for the current full set when the caller actually needs it (e.g., debugging AST structure).
106
+
107
+ This is also the cheapest improvement on the list — table-projection at the response layer, no logic changes.
108
+
109
+ ## 6. Consistent shape across verbs
110
+
111
+ `explore` returns markdown with sections. `find` returns a markdown table. `investigate` returns markdown with sub-headings and embedded tables. Each is sensible alone but reading three in one session means three different parse strategies.
112
+
113
+ **Proposal**
114
+
115
+ Either fully commit to markdown (with consistent section-header conventions across verbs) or move to JSON+embedded-markdown. The current mix forces callers (human or agent) to context-switch.
116
+
117
+ ## 7. Truncation signaling
118
+
119
+ `explore`'s doc table did this correctly: `--- omitted 1283 of 1293 rows ---`. But `find` and `investigate` don't always indicate when results are capped at `max_results`/`max_lines`. A caller seeing 50 rows can't tell whether 50 is the universe or just the first page.
120
+
121
+ **Proposal**
122
+
123
+ Standardize a footer like `[showing N of M; pass max_results=K to widen]` on every verb that has a default cap.
124
+
125
+ ## Priority suggestion
126
+
127
+ If you only do two of these:
128
+
129
+ 1. **#5 (compact columns)** — biggest reduction in noise across every `find`/`find_names` call.
130
+ 2. **#2 (recent_activity path leak)** — cleanest correctness bug; one-liner fix.
131
+
132
+ If three: add **#3 (investigate source truncation)** — that one bit me directly and the "Definition end_line vs Source last line" mismatch is unambiguous.
133
+
134
+ The selector-qualification work (#4) is the most expressive improvement but also the most design. Worth its own thread.
@@ -64,13 +64,23 @@ Returns:
64
64
 
65
65
  This one tool gives you the definition *and* its neighborhood. Usually enough to decide if this is the right function to focus on.
66
66
 
67
- **`investigate` is scoped to the MCP server's project root.** If you're exploring a different project (absolute paths in Phase 1), `investigate` will return "No definition found" — it doesn't know about the other project. Substitute Phase 2 with manual queries:
67
+ **Working on a project other than the server's own?** Two scoping rules:
68
+
69
+ - **Full-text search** (`search_code` / `search_content` / `search_docs`) defaults to the
70
+ server's project and ignores the structural `source` glob. To FTS a *different* repo, pass
71
+ **`root=<repository directory>`** (absolute path) — squackit indexes it on first use and
72
+ caches it. `search_code(query="...", root="/abs/repo")` works across repos.
73
+ - **`investigate`** is scoped to the server's project; on a different project it returns "No
74
+ definition found." Substitute Phase 2 with the structural tools (`find` / `view` /
75
+ `find_names` / `read_source`), which take an absolute `source` glob per call:
68
76
 
69
77
  ```
70
78
  # Equivalent of investigate(name="X") for a non-rooted project:
71
79
  find(source="/abs/path/**/*.py", selector=".fn#X") # → definition + line range
72
80
  view(source="/abs/path/**/*.py", selector=".fn#X") # → source body
73
- # (callers/callees: not currently available without investigate)
81
+ find(source="/abs/path/**/*.py", selector=".call#X") # → callers: each row's scope.function is the enclosing caller
82
+ # (a function passed as a VALUE — pool.submit(X), a decorator, a callback — is not a call
83
+ # site, so .call#X won't list it; search the bare name "X" to find those references.)
74
84
  ```
75
85
 
76
86
  **When to repeat vs. pivot:**
@@ -121,14 +121,32 @@ pluck(argv="--plugin AstViewer src/auth.py find .fn#validate_token view")
121
121
 
122
122
  ## Selector cheat sheet
123
123
 
124
- Same CSS grammar as the single-purpose tools:
125
-
126
- - `.fn` / `.class` / `.call` — by kind
127
- - `#name` — by exact name (e.g. `.fn#main`)
128
- - `[attr]` — by attribute (e.g. `.fn[name*="test"]`)
129
- - `.class > .fn` — direct children
130
- - `.fn:has(.call#foo)` — functions that call foo
131
- - `.class[name*="Test"]` — classes with "Test" in name
124
+ Same CSS grammar as the single-purpose tools. **Verified working:**
125
+
126
+ - `.fn` / `.class` / `.call` — by kind (`.function` / `.func` / `.def` all alias `.fn`)
127
+ - `#name` — by exact name: `.fn#main` (function def `main`), `.class#Foo`, and crucially
128
+ `.call#foo` (every **call site** of `foo(...)`)
129
+ - `[attr]` — attribute match: `.fn[name*="test"]`, `.fn[name^=test_]`, `.class[name*="Test"]`
130
+ - `.class > .fn` — direct children (a class's methods)
131
+
132
+ ### Finding callers — `.call#NAME` or `.fn:has(.call#NAME)`
133
+
134
+ Two reliable ways to find who calls a function:
135
+
136
+ - `find ".call#get_notifications"` → every call site of `get_notifications()`. Each result
137
+ row's `scope.function` field is the **enclosing function** (i.e. the caller). Exact and cheap.
138
+ - `find ".fn:has(.call#get_notifications)"` → the caller functions directly. (This now works:
139
+ the `#NAME` filter inside `:has` is honored — selector matching is delegated to sitting_duck's
140
+ `ast_select`. Earlier pluckit versions dropped it and over-matched; that's fixed.)
141
+ - A function passed as a **value** (`pool.submit(fn)`, a decorator, a callback) is **not** a
142
+ call site, so neither form lists it. To find those references, search the bare name.
143
+
144
+ ### Filtering by decorator
145
+
146
+ Use `:decorated` (functions with any decorator) or filter on the `annotations` /
147
+ `modifiers` column that `find` / `find_names` return (e.g. `['@mcp.tool()']`, `['@property']`)
148
+ when you need a specific decorator. Likewise attribute-chain calls (`os.environ.get(...)`) match
149
+ on the final name: try `.call#get`, but verify with a quick `find` before relying on it.
132
150
 
133
151
  Escape whitespace in args with quotes: `containing "return None"`.
134
152
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "squackit"
3
- version = "0.5.0"
3
+ version = "0.7.0"
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"
@@ -38,7 +38,7 @@ def setup_lackpy():
38
38
  """Initialize lackpy service with squackit tools."""
39
39
  project_root = Path(__file__).parent.parent
40
40
  sys.path.insert(0, str(project_root))
41
- from lackpy import LackpyService
41
+ from lackpy.service import LackpyService
42
42
  from squackit.lackpy_integration import register_squackit_kit
43
43
 
44
44
  svc = LackpyService(workspace=project_root)
@@ -6,7 +6,7 @@ work to squackit.
6
6
 
7
7
  Usage::
8
8
 
9
- from lackpy import LackpyService
9
+ from lackpy.service import LackpyService
10
10
  from squackit.lackpy_integration import register_squackit_kit
11
11
 
12
12
  svc = LackpyService()
@@ -153,7 +153,7 @@ def _read_source_tool(file_path: str, lines: str | None = None) -> str:
153
153
 
154
154
 
155
155
  def _make_tool_specs() -> list[Any]:
156
- from lackpy import ArgSpec, ToolSpec # type: ignore
156
+ from lackpy.kit.toolbox import ArgSpec, ToolSpec # type: ignore
157
157
 
158
158
  return [
159
159
  ToolSpec(
@@ -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__}")