Skip to content

Commit 0876fed

Browse files
refactor: code quality sweep — circular deps, deduplication, agent config consistency
Architecture: - Break circular deps: new output.py (console + energy_to_label), new services/backlog.py - Shared DB connection factory: new db.py connect_db(), deduplicated 3 callers - escape_fts_query: semantic_search.py now imports from query_utils Agent config contradictions (5 fixes): - Break thresholds: energy-adaptive references replace hardcoded High-only - Session state init: removed dual-init race in Claude agent - Voice toggle: added to Claude, Gemini, OpenCode agents - AuDHD support: Gemini/OpenCode reference shared doc instead of stale inline copy - Spaced repetition: max 3 per session standardised across all agents + shared protocol Test infrastructure: - addopts: -m 'not integration' (matches CI, integration tests opt-in) - mcp[cli] in root dev deps (14 MCP tests now run) - test_history.py: importorskip for cross-package import Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 42ecc81 commit 0876fed

20 files changed

Lines changed: 168 additions & 120 deletions

File tree

‎agents/claude/socratic-mentor.md‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,9 @@ Exceptions: explicit "just show me", 4+ rounds stuck, pure syntax lookup, boiler
4949

5050
## Session State Management
5151

52-
At the start of each session, create the session state file:
53-
```bash
54-
mkdir -p ~/.config/studyctl
55-
cat > ~/.config/studyctl/session-state.json << 'EOF'
56-
{"energy": "medium", "topic": "", "pomodoro": null}
57-
EOF
58-
```
52+
Session state is created by `studyctl session start`. Do NOT manually create the file.
5953

60-
When the user specifies their energy level, update the state file:
54+
When the user specifies their energy level mid-session, update the state file:
6155
```bash
6256
python3 -c "import json; from pathlib import Path; p=Path.home()/'.config/studyctl/session-state.json'; d=json.loads(p.read_text()); d['energy']='LEVEL'; p.write_text(json.dumps(d))"
6357
```
@@ -71,8 +65,7 @@ This state is read by the Claude Code status line to show persistent session inf
7165

7266
Follow `agents/shared/session-protocol.md`. Summary:
7367

74-
1. Initialise the session state file (see above)
75-
2. Run system checks:
68+
1. Run system checks:
7669
```bash
7770
studyctl resume # Where you left off
7871
studyctl status # Check sync state
@@ -89,7 +82,7 @@ Follow `agents/shared/session-protocol.md`. Summary:
8982
## Session Types
9083

9184
- **Study session:** arrival → state check → system check → topic → Socratic session → record progress
92-
- **Spaced review:** `studyctl review` → quiz overdue topics (interleave if 2+ due) → record scores
85+
- **Spaced review:** `studyctl review` → quiz overdue topics (max 3 per session, interleave if 2+ due) → record scores
9386
- **Body doubling (active):** agree goal + time → start/mid/end check-ins
9487
- **Body doubling (async):** periodic low-demand check-ins, no teaching
9588
- **Ad-hoc question:** identify topic → respond Socratically
@@ -170,6 +163,13 @@ Requires: pandoc, @mermaid-js/mermaid-cli for markdown→PDF with diagram suppor
170163

171164
---
172165

166+
## Voice Output (study-speak)
167+
168+
The learner can toggle voice on/off with `/speak-start` and `/speak-stop`.
169+
Follow the full rules in `agents/shared/session-protocol.md` (Voice Output section).
170+
171+
---
172+
173173
## Anti-Patterns to Avoid
174174

175175
- **The Encyclopedia Response**: Too much information at once

‎agents/gemini/study-mentor.md‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,13 @@ Then follow `session-protocol.md`: combined state check (energy, mood, setup), a
6969
## Session Types
7070

7171
- **Study session:** arrival → state check → system check → topic → Socratic session → record progress
72-
- **Spaced review:** `studyctl review` → quiz overdue topics (max 3, interleave if 2+ due) → record
72+
- **Spaced review:** `studyctl review` → quiz overdue topics (max 3 per session, interleave if 2+ due) → record
7373
- **Body doubling:** agree goal + time → start/mid/end check-ins
7474
- **Ad-hoc question:** identify topic → respond Socratically
7575

7676
## AuDHD Support (Always Active)
7777

78-
- **Bottom-up processing**: Concrete example first, then pattern, then principle
79-
- **Executive function**: Explicit starting points, time-boxes, numbered steps, summaries every 3-5 exchanges
80-
- **RSD/Imposter syndrome**: Reframe mistakes as exploration, bridge to infrastructure experience
81-
- **Overload prevention**: Max 3-4 concepts, tables over prose, TL;DR at top, mermaid diagrams
82-
- **Hyperfocus**: Time warnings, exit points, hydration/food reminders
83-
- **Emotional regulation**: Micro-celebrations for genuine progress, sensory checks at 45+ min
84-
- **Transition support**: Summarise when switching, parking lot for tangents
78+
See `agents/shared/audhd-framework.md` for the complete methodology. Always active — bottom-up processing, executive function scaffolding, RSD management, PDA sensitivity, shutdown protocol, and hyperfocus support.
8579

8680
## End-of-Session Protocol
8781

@@ -90,14 +84,20 @@ Follow `wind-down-protocol.md`:
9084
2. End session: `studyctl session end --notes "<summary>"` — flushes parking lot to DB, exports to Obsidian
9185
3. Suggest next review based on spaced repetition intervals
9286
4. Offer calendar blocks: `studyctl schedule-blocks`
93-
5. If session was 25+ min, remind to take a break
87+
5. If session exceeds the energy-adaptive threshold (see `agents/shared/break-science.md`), remind to take a break
9488
6. Parking lot: note tangential topics worth revisiting
9589

9690
## Break Reminders
9791

98-
- 25 min: "Good time for a 5-minute break."
99-
- 50 min: "Take a proper break before continuing."
100-
- 90 min: "You should stop here and come back fresh."
92+
Follow the energy-adaptive schedule in `agents/shared/break-science.md`:
93+
- High energy: 25/50/90 min
94+
- Medium energy: 20/40/75 min
95+
- Low energy: 15/30/60 min
96+
97+
## Voice Output (study-speak)
98+
99+
The learner can toggle voice on/off with `@speak-start` and `@speak-stop`.
100+
Follow the full rules in `agents/shared/session-protocol.md` (Voice Output section).
101101

102102
## Anti-Patterns to Avoid
103103

‎agents/kiro/study-mentor/persona.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Run `studyctl config show` to see your configured notebook IDs.
3838
## Session Types
3939

4040
**Study session:** arrival → state check → system check → topic → Socratic session → record
41-
**Spaced review:** arrival → state check → `studyctl review` → quiz overdue topics (interleave if 2+ due) → record
41+
**Spaced review:** arrival → state check → `studyctl review` → quiz overdue topics (max 3 per session, interleave if 2+ due) → record
4242
**Body doubling (active):** agree goal + time → start/mid/end check-ins
4343
**Body doubling (async):** "I'm working, not studying. Check in on me." → periodic low-demand check-ins
4444
**Ad-hoc question:** identify topic → query NotebookLM → respond Socratically

‎agents/opencode/study-mentor.md‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,19 +73,13 @@ Then follow `session-protocol.md`: combined state check (energy, mood, setup), a
7373
## Session Types
7474

7575
- **Study session:** arrival → state check → system check → topic → Socratic session → record progress
76-
- **Spaced review:** `studyctl review` → quiz overdue topics (max 3, interleave if 2+ due) → record
76+
- **Spaced review:** `studyctl review` → quiz overdue topics (max 3 per session, interleave if 2+ due) → record
7777
- **Body doubling:** agree goal + time → start/mid/end check-ins
7878
- **Ad-hoc question:** identify topic → respond Socratically
7979

8080
## AuDHD Support (Always Active)
8181

82-
- **Bottom-up processing**: Concrete example first, then pattern, then principle
83-
- **Executive function**: Explicit starting points, time-boxes, numbered steps, summaries every 3-5 exchanges
84-
- **RSD/Imposter syndrome**: Reframe mistakes as exploration, bridge to infrastructure experience
85-
- **Overload prevention**: Max 3-4 concepts, tables over prose, TL;DR at top, mermaid diagrams
86-
- **Hyperfocus**: Time warnings, exit points, hydration/food reminders
87-
- **Emotional regulation**: Micro-celebrations for genuine progress, sensory checks at 45+ min
88-
- **Transition support**: Summarise when switching, parking lot for tangents
82+
See `agents/shared/audhd-framework.md` for the complete methodology. Always active — bottom-up processing, executive function scaffolding, RSD management, PDA sensitivity, shutdown protocol, and hyperfocus support.
8983

9084
## End-of-Session Protocol
9185

@@ -94,14 +88,20 @@ Follow `wind-down-protocol.md`:
9488
2. End session: `studyctl session end --notes "<summary>"` — flushes parking lot to DB, exports to Obsidian
9589
3. Suggest next review based on spaced repetition intervals
9690
4. Offer calendar blocks: `studyctl schedule-blocks`
97-
5. If session was 25+ min, remind to take a break
91+
5. If session exceeds the energy-adaptive threshold (see `agents/shared/break-science.md`), remind to take a break
9892
6. Parking lot: note tangential topics worth revisiting
9993

10094
## Break Reminders
10195

102-
- 25 min: "Good time for a 5-minute break."
103-
- 50 min: "Take a proper break before continuing."
104-
- 90 min: "You should stop here and come back fresh."
96+
Follow the energy-adaptive schedule in `agents/shared/break-science.md`:
97+
- High energy: 25/50/90 min
98+
- Medium energy: 20/40/75 min
99+
- Low energy: 15/30/60 min
100+
101+
## Voice Output (study-speak)
102+
103+
The learner can toggle voice on/off with `@speak-start` and `@speak-stop`.
104+
Follow the full rules in `agents/shared/session-protocol.md` (Voice Output section).
105105

106106
## Anti-Patterns to Avoid
107107

‎agents/shared/session-protocol.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ If `studyctl streaks` shows a current streak, mention it: "Day [N] of your study
7878

7979
Based on state check + what's due, propose a session plan:
8080

81-
- If spaced repetition items are due → review session (interleave related topics)
81+
- If spaced repetition items are due → review session (max 3 topics per session, interleave related topics; if more due, prioritise longest-overdue)
8282
- If struggle topics detected → targeted practice with extra scaffolding
8383
- If nothing due + high energy → new material
8484
- If low energy or flat → body doubling or light review

‎packages/agent-session-tools/src/agent_session_tools/semantic_search.py‎

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -70,26 +70,7 @@ class SearchContext:
7070
exclude_session_ids: list[str] = field(default_factory=list)
7171

7272

73-
def escape_fts_query(query: str) -> str:
74-
"""Escape a query string for FTS5 MATCH.
75-
76-
With porter stemming enabled, we can use simpler queries that match variants.
77-
For example: "create" will match "created", "creating", "creates".
78-
"""
79-
query = query.strip().strip('"').strip("'")
80-
81-
# If query contains FTS operators (AND, OR, NOT), use as-is
82-
if any(op in query.upper() for op in [" AND ", " OR ", " NOT "]):
83-
return query
84-
85-
# Escape quotes
86-
escaped = query.replace('"', '""')
87-
88-
# Multi-word queries become phrases
89-
if " " in escaped:
90-
return f'"{escaped}"'
91-
92-
return escaped
73+
from agent_session_tools.query_utils import escape_fts_query # noqa: E402, F401
9374

9475

9576
def hybrid_search(

‎packages/studyctl/pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ packages = ["src/studyctl"]
4949

5050
[tool.pytest.ini_options]
5151
testpaths = ["tests"]
52-
addopts = "--tb=short"
52+
addopts = "--tb=short -m 'not integration'"
5353
markers = [
5454
"integration: requires external infrastructure (tmux, real DB, network)",
5555
]

‎packages/studyctl/src/studyctl/cli/_session.py‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,9 @@ def session_start(topic: str, energy: int) -> None:
3434

3535
_ensure_session_dir()
3636

37-
# Map integer energy to label for history.py
38-
if energy <= 3:
39-
energy_label = "low"
40-
elif energy <= 7:
41-
energy_label = "medium"
42-
else:
43-
energy_label = "high"
37+
from studyctl.output import energy_to_label
38+
39+
energy_label = energy_to_label(energy)
4440

4541
study_id = start_study_session(topic, energy_label)
4642
if not study_id:

‎packages/studyctl/src/studyctl/cli/_shared.py‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@
55
import subprocess
66
from pathlib import Path
77

8-
from rich.console import Console
9-
8+
from studyctl.output import console
109
from studyctl.topics import Topic, get_topics
1110

12-
console = Console()
13-
1411
# Topic keywords for session DB queries
1512
TOPIC_KEYWORDS = {
1613
"python": [

‎packages/studyctl/src/studyctl/cli/_study.py‎

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77

88
from __future__ import annotations
99

10+
import logging
1011
from datetime import UTC, datetime
1112

1213
import click
1314

1415
from studyctl.cli._shared import console
1516

17+
logger = logging.getLogger(__name__)
18+
1619

1720
def _agent_names() -> list[str]:
1821
"""Registered agent names for CLI --agent choices."""
@@ -202,40 +205,20 @@ def _auto_persist_struggled(
202205
) -> None:
203206
"""Persist struggled topics from session-topics.md to the backlog.
204207
205-
Uses FCIS pattern -- plan_auto_persist decides what to persist,
206-
then we execute by calling park_topic for each action.
208+
Thin CLI wrapper over services.backlog.auto_persist_struggled —
209+
adds console output.
207210
"""
208-
import contextlib
209-
210-
with contextlib.suppress(Exception):
211-
from studyctl.logic.backlog_logic import plan_auto_persist
212-
from studyctl.parking import get_parked_topics, park_topic
213-
214-
# Gather existing questions for this session to deduplicate
215-
existing = get_parked_topics(study_session_id=study_session_id)
216-
existing_questions = {t["question"] for t in existing}
217-
218-
# Decide
219-
actions = plan_auto_persist(topic_entries, existing_questions, study_session_id)
220-
221-
# Execute
222-
persisted = 0
223-
for action in actions:
224-
result = park_topic(
225-
question=action.question,
226-
topic_tag=action.topic_tag,
227-
context=action.context,
228-
study_session_id=action.study_session_id,
229-
source=action.source,
230-
)
231-
if result:
232-
persisted += 1
211+
try:
212+
from studyctl.services.backlog import auto_persist_struggled
233213

214+
persisted = auto_persist_struggled(study_session_id, topic_entries)
234215
if persisted:
235216
console.print(
236217
f"[dim]Saved {persisted} struggled "
237218
f"topic{'s' if persisted != 1 else ''} to backlog[/dim]"
238219
)
220+
except Exception:
221+
logger.exception("Failed to auto-persist struggled topics to backlog")
239222

240223

241224
def _handle_start(
@@ -313,12 +296,9 @@ def _handle_start(
313296

314297
# --- Create DB session ---
315298

316-
if energy <= 3:
317-
energy_label = "low"
318-
elif energy <= 7:
319-
energy_label = "medium"
320-
else:
321-
energy_label = "high"
299+
from studyctl.output import energy_to_label
300+
301+
energy_label = energy_to_label(energy)
322302

323303
study_id = start_study_session(topic, energy_label)
324304
if not study_id:

0 commit comments

Comments
 (0)