docs(01): create phase plan
Phase 01: Session & Process Foundation - 3 plan(s) in 2 wave(s) - 2 parallel (wave 1), 1 sequential (wave 2) - Ready for execution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a522a108ca
commit
0baaeb26b5
4 changed files with 831 additions and 3 deletions
|
|
@ -29,10 +29,12 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||||
3. Each session has isolated directory at `~/telegram/sessions/<name>/` with metadata and conversation log
|
3. Each session has isolated directory at `~/telegram/sessions/<name>/` with metadata and conversation log
|
||||||
4. Claude Code subprocess spawns in session directory and processes input without pipe deadlock
|
4. Claude Code subprocess spawns in session directory and processes input without pipe deadlock
|
||||||
5. Subprocess terminates cleanly on session switch with no zombie processes
|
5. Subprocess terminates cleanly on session switch with no zombie processes
|
||||||
**Plans**: TBD
|
**Plans:** 3 plans
|
||||||
|
|
||||||
Plans:
|
Plans:
|
||||||
- [ ] TBD
|
- [ ] 01-01-PLAN.md -- Session manager module and persona library
|
||||||
|
- [ ] 01-02-PLAN.md -- Claude Code subprocess engine
|
||||||
|
- [ ] 01-03-PLAN.md -- Bot command integration (/new, /session, message routing)
|
||||||
|
|
||||||
### Phase 2: Telegram Integration
|
### Phase 2: Telegram Integration
|
||||||
**Goal**: Messages flow bidirectionally between Telegram and Claude with file support and status feedback
|
**Goal**: Messages flow bidirectionally between Telegram and Claude with file support and status feedback
|
||||||
|
|
@ -85,7 +87,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4
|
||||||
|
|
||||||
| Phase | Plans Complete | Status | Completed |
|
| Phase | Plans Complete | Status | Completed |
|
||||||
|-------|----------------|--------|-----------|
|
|-------|----------------|--------|-----------|
|
||||||
| 1. Session & Process Foundation | 0/TBD | Not started | - |
|
| 1. Session & Process Foundation | 0/3 | Planned | - |
|
||||||
| 2. Telegram Integration | 0/TBD | Not started | - |
|
| 2. Telegram Integration | 0/TBD | Not started | - |
|
||||||
| 3. Lifecycle Management | 0/TBD | Not started | - |
|
| 3. Lifecycle Management | 0/TBD | Not started | - |
|
||||||
| 4. Output Modes | 0/TBD | Not started | - |
|
| 4. Output Modes | 0/TBD | Not started | - |
|
||||||
|
|
|
||||||
305
.planning/phases/01-session-process-foundation/01-01-PLAN.md
Normal file
305
.planning/phases/01-session-process-foundation/01-01-PLAN.md
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
---
|
||||||
|
phase: 01-session-process-foundation
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- telegram/session_manager.py
|
||||||
|
- telegram/personas/default.json
|
||||||
|
- telegram/personas/brainstorm.json
|
||||||
|
- telegram/personas/planner.json
|
||||||
|
- telegram/personas/research.json
|
||||||
|
autonomous: true
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "SessionManager.create_session('test') creates directory at ~/telegram/sessions/test/ with metadata.json"
|
||||||
|
- "SessionManager.create_session('test', persona='brainstorm') copies brainstorm persona into session directory"
|
||||||
|
- "SessionManager.switch_session('test') updates active session and returns previous session name"
|
||||||
|
- "SessionManager.get_session('test') returns session metadata including name, status, timestamps"
|
||||||
|
- "Session names are validated: alphanumeric, hyphens, underscores only"
|
||||||
|
- "Persona library templates exist at ~/telegram/personas/ with at least default, brainstorm, planner, research"
|
||||||
|
artifacts:
|
||||||
|
- path: "telegram/session_manager.py"
|
||||||
|
provides: "Session lifecycle management"
|
||||||
|
min_lines: 80
|
||||||
|
contains: "class SessionManager"
|
||||||
|
- path: "telegram/personas/default.json"
|
||||||
|
provides: "Default persona template"
|
||||||
|
contains: "system_prompt"
|
||||||
|
- path: "telegram/personas/brainstorm.json"
|
||||||
|
provides: "Brainstorming persona template"
|
||||||
|
contains: "system_prompt"
|
||||||
|
key_links:
|
||||||
|
- from: "telegram/session_manager.py"
|
||||||
|
to: "telegram/personas/"
|
||||||
|
via: "persona library lookup on create_session"
|
||||||
|
pattern: "personas.*json"
|
||||||
|
- from: "telegram/session_manager.py"
|
||||||
|
to: "telegram/sessions/"
|
||||||
|
via: "directory creation and metadata writes"
|
||||||
|
pattern: "sessions.*metadata\\.json"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create the session management module and persona library that provides the filesystem foundation for multi-session Claude Code conversations.
|
||||||
|
|
||||||
|
Purpose: Sessions are the core abstraction — each session is an isolated directory where a Claude Code subprocess will run (Plan 02) with its own conversation history, metadata, and persona configuration. This plan builds the session CRUD operations and persona template system.
|
||||||
|
|
||||||
|
Output: `telegram/session_manager.py` module and `telegram/personas/` directory with reusable persona templates.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@/home/mikkel/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@/home/mikkel/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/01-session-process-foundation/01-CONTEXT.md
|
||||||
|
@.planning/phases/01-session-process-foundation/01-RESEARCH.md
|
||||||
|
@telegram/bot.py
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create SessionManager module</name>
|
||||||
|
<files>telegram/session_manager.py</files>
|
||||||
|
<action>
|
||||||
|
Create `telegram/session_manager.py` with a `SessionManager` class that manages session lifecycle.
|
||||||
|
|
||||||
|
**Session directory structure:**
|
||||||
|
```
|
||||||
|
~/telegram/sessions/<name>/
|
||||||
|
metadata.json # Session state
|
||||||
|
persona.json # Session persona (copied from library or custom)
|
||||||
|
.claude/ # Auto-created by Claude Code CLI
|
||||||
|
```
|
||||||
|
|
||||||
|
**SessionManager class design:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
class SessionManager:
|
||||||
|
def __init__(self, base_dir: Path = None):
|
||||||
|
# base_dir defaults to ~/telegram/sessions/
|
||||||
|
# Also tracks personas_dir at ~/telegram/personas/
|
||||||
|
# Tracks active_session (str or None)
|
||||||
|
# Tracks all sessions dict[str, SessionMetadata]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required methods:**
|
||||||
|
|
||||||
|
1. `create_session(name: str, persona: str = None) -> Path`
|
||||||
|
- Validate name: regex `^[a-zA-Z0-9_-]+$`, max 50 chars
|
||||||
|
- If session already exists: raise ValueError with clear message (least-surprising: don't silently overwrite)
|
||||||
|
- Create directory at `sessions/<name>/`
|
||||||
|
- If persona specified: look up `personas/<persona>.json`, copy to `sessions/<name>/persona.json`
|
||||||
|
- If persona not specified: copy `personas/default.json` to session
|
||||||
|
- Write `metadata.json` with fields:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "session-name",
|
||||||
|
"created": "ISO-8601 timestamp",
|
||||||
|
"last_active": "ISO-8601 timestamp",
|
||||||
|
"persona": "persona-name-or-null",
|
||||||
|
"pid": null,
|
||||||
|
"status": "idle"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Return session directory Path
|
||||||
|
- Status values: "idle" (no process), "active" (has running process, is current), "suspended" (has running process, not current)
|
||||||
|
|
||||||
|
2. `switch_session(name: str) -> str | None`
|
||||||
|
- If session doesn't exist: raise ValueError
|
||||||
|
- If already active: return None (no-op)
|
||||||
|
- Mark current active session as "suspended" in metadata (process stays alive per CONTEXT.md decision)
|
||||||
|
- Mark new session as "active" in metadata
|
||||||
|
- Update `last_active` timestamp on new session
|
||||||
|
- Update `self.active_session`
|
||||||
|
- Return previous session name (or None if no previous)
|
||||||
|
|
||||||
|
3. `get_session(name: str) -> dict`
|
||||||
|
- Read and return metadata.json contents for named session
|
||||||
|
- Raise ValueError if session doesn't exist
|
||||||
|
|
||||||
|
4. `list_sessions() -> list[dict]`
|
||||||
|
- Return list of all session metadata, sorted by last_active (most recent first)
|
||||||
|
|
||||||
|
5. `get_active_session() -> str | None`
|
||||||
|
- Return name of active session or None
|
||||||
|
|
||||||
|
6. `update_session(name: str, **kwargs) -> None`
|
||||||
|
- Update specific fields in session metadata (used by subprocess module to set PID, status)
|
||||||
|
|
||||||
|
7. `session_exists(name: str) -> bool`
|
||||||
|
- Check if session directory exists
|
||||||
|
|
||||||
|
8. `get_session_dir(name: str) -> Path`
|
||||||
|
- Return Path to session directory
|
||||||
|
|
||||||
|
9. `load_persona(name: str) -> dict`
|
||||||
|
- Load persona JSON from library (~/telegram/personas/<name>.json)
|
||||||
|
- Return persona dict or raise FileNotFoundError
|
||||||
|
|
||||||
|
**Implementation notes:**
|
||||||
|
- Use `pathlib.Path` throughout
|
||||||
|
- Use `json` stdlib for metadata reads/writes
|
||||||
|
- Make metadata reads lazy (read from disk each time to avoid stale state)
|
||||||
|
- Add `logging` using `logging.getLogger(__name__)`
|
||||||
|
- Include type hints for all methods
|
||||||
|
- Add module docstring explaining the session model
|
||||||
|
|
||||||
|
**DO NOT:**
|
||||||
|
- Import or depend on claude_subprocess.py (that's Plan 02)
|
||||||
|
- Add Telegram-specific code (that's Plan 03)
|
||||||
|
- Implement idle timeout (that's Phase 3)
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
```bash
|
||||||
|
cd ~/homelab
|
||||||
|
source ~/venv/bin/activate
|
||||||
|
python3 -c "
|
||||||
|
from telegram.session_manager import SessionManager
|
||||||
|
sm = SessionManager()
|
||||||
|
# Create session
|
||||||
|
path = sm.create_session('test-session')
|
||||||
|
assert path.exists()
|
||||||
|
assert (path / 'metadata.json').exists()
|
||||||
|
assert (path / 'persona.json').exists()
|
||||||
|
meta = sm.get_session('test-session')
|
||||||
|
assert meta['name'] == 'test-session'
|
||||||
|
assert meta['status'] == 'idle'
|
||||||
|
# Switch session
|
||||||
|
sm.create_session('second-session')
|
||||||
|
prev = sm.switch_session('second-session')
|
||||||
|
assert prev is None # No previous active
|
||||||
|
assert sm.get_active_session() == 'second-session'
|
||||||
|
prev = sm.switch_session('test-session')
|
||||||
|
assert prev == 'second-session'
|
||||||
|
# List sessions
|
||||||
|
sessions = sm.list_sessions()
|
||||||
|
assert len(sessions) >= 2
|
||||||
|
# Validation
|
||||||
|
try:
|
||||||
|
sm.create_session('bad name!')
|
||||||
|
assert False, 'Should have raised ValueError'
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
# Cleanup
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(path.parent / 'test-session')
|
||||||
|
shutil.rmtree(path.parent / 'second-session')
|
||||||
|
print('All session manager tests passed!')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
</verify>
|
||||||
|
<done>SessionManager creates isolated session directories with metadata, handles persona inheritance from library, validates names, switches active session correctly, and lists all sessions sorted by activity.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create persona library with default templates</name>
|
||||||
|
<files>
|
||||||
|
telegram/personas/default.json
|
||||||
|
telegram/personas/brainstorm.json
|
||||||
|
telegram/personas/planner.json
|
||||||
|
telegram/personas/research.json
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Create the persona library directory and four starter personas at `~/homelab/telegram/personas/`.
|
||||||
|
|
||||||
|
**Persona JSON schema:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "persona-display-name",
|
||||||
|
"description": "One-line description of this persona's purpose",
|
||||||
|
"system_prompt": "The system prompt that shapes Claude's behavior in this session",
|
||||||
|
"settings": {
|
||||||
|
"model": "claude-sonnet-4-20250514",
|
||||||
|
"max_turns": 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Personas to create:**
|
||||||
|
|
||||||
|
1. **default.json** - General-purpose assistant
|
||||||
|
- system_prompt: "You are Claude, an AI assistant helping Mikkel manage his homelab infrastructure. You have full access to the management container's tools and can SSH to other containers. Be helpful, thorough, and proactive about suggesting improvements. When making changes, explain what you're doing and why."
|
||||||
|
- settings.model: "claude-sonnet-4-20250514" (cost-effective default)
|
||||||
|
- settings.max_turns: 25
|
||||||
|
|
||||||
|
2. **brainstorm.json** - Creative ideation mode
|
||||||
|
- system_prompt: "You are in brainstorming mode. Generate ideas freely without filtering. Build on previous ideas. Explore unconventional approaches. Ask probing questions to understand the problem space better. Don't worry about feasibility yet - that comes later. Output ideas as bullet lists for easy scanning."
|
||||||
|
- settings.model: "claude-sonnet-4-20250514"
|
||||||
|
- settings.max_turns: 50 (longer conversations for ideation)
|
||||||
|
|
||||||
|
3. **planner.json** - Structured planning mode
|
||||||
|
- system_prompt: "You are in planning mode. Break down complex tasks into clear, actionable steps. Identify dependencies and ordering. Estimate effort and flag risks. Use structured formats (numbered lists, tables) for clarity. Ask clarifying questions about requirements before diving into solutions."
|
||||||
|
- settings.model: "claude-sonnet-4-20250514"
|
||||||
|
- settings.max_turns: 30
|
||||||
|
|
||||||
|
4. **research.json** - Deep investigation mode
|
||||||
|
- system_prompt: "You are in research mode. Investigate topics thoroughly. Check documentation, source code, and configuration files. Cross-reference information. Cite your sources (file paths, URLs). Distinguish between facts and inferences. Summarize findings clearly with actionable recommendations."
|
||||||
|
- settings.model: "claude-sonnet-4-20250514"
|
||||||
|
- settings.max_turns: 30
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- The `settings` block will be consumed by the subprocess module (Plan 02) to configure Claude Code CLI flags
|
||||||
|
- Keep system_prompts concise but distinctive — each persona should feel like a different "mode"
|
||||||
|
- Use claude-sonnet-4-20250514 as default model (good balance of capability and cost for Telegram-driven sessions)
|
||||||
|
- The schema is intentionally simple for Phase 1; can be extended in future phases
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
```bash
|
||||||
|
cd ~/homelab
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
personas_dir = Path('telegram/personas')
|
||||||
|
assert personas_dir.exists(), 'personas directory missing'
|
||||||
|
|
||||||
|
required = ['default.json', 'brainstorm.json', 'planner.json', 'research.json']
|
||||||
|
for name in required:
|
||||||
|
path = personas_dir / name
|
||||||
|
assert path.exists(), f'{name} missing'
|
||||||
|
data = json.loads(path.read_text())
|
||||||
|
assert 'name' in data, f'{name} missing name field'
|
||||||
|
assert 'description' in data, f'{name} missing description field'
|
||||||
|
assert 'system_prompt' in data, f'{name} missing system_prompt field'
|
||||||
|
assert 'settings' in data, f'{name} missing settings field'
|
||||||
|
assert 'model' in data['settings'], f'{name} missing settings.model'
|
||||||
|
assert 'max_turns' in data['settings'], f'{name} missing settings.max_turns'
|
||||||
|
print(f' {name}: OK ({data[\"name\"]})')
|
||||||
|
|
||||||
|
print('All persona templates valid!')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
</verify>
|
||||||
|
<done>Four persona templates exist in ~/homelab/telegram/personas/ with valid JSON schema (name, description, system_prompt, settings.model, settings.max_turns). Each persona has a distinct system_prompt that shapes Claude's behavior differently.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. `telegram/session_manager.py` exists with `SessionManager` class
|
||||||
|
2. `telegram/personas/` directory contains 4 valid persona JSON files
|
||||||
|
3. Creating a session writes metadata.json and copies persona to session directory
|
||||||
|
4. Session switching updates active session and marks previous as suspended
|
||||||
|
5. Session name validation rejects invalid characters
|
||||||
|
6. No imports from claude_subprocess or telegram bot modules
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- SessionManager can create, list, switch, and query sessions purely via filesystem operations
|
||||||
|
- Persona library provides 4 distinct templates with consistent schema
|
||||||
|
- Session directories are fully isolated (each has own metadata.json and persona.json)
|
||||||
|
- All verification scripts pass without errors
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/01-session-process-foundation/01-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
252
.planning/phases/01-session-process-foundation/01-02-PLAN.md
Normal file
252
.planning/phases/01-session-process-foundation/01-02-PLAN.md
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
---
|
||||||
|
phase: 01-session-process-foundation
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- telegram/claude_subprocess.py
|
||||||
|
autonomous: true
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "ClaudeSubprocess spawns Claude Code CLI in a given session directory using asyncio.create_subprocess_exec"
|
||||||
|
- "Stdout and stderr are read concurrently via asyncio.gather -- no pipe deadlock occurs"
|
||||||
|
- "Process termination uses terminate() + wait_for() with timeout fallback to kill() -- no zombies"
|
||||||
|
- "Messages queued while Claude is processing are sent after current response completes"
|
||||||
|
- "If Claude Code crashes, it auto-restarts with --continue flag and a notification callback fires"
|
||||||
|
- "Stream-json output is parsed line-by-line, routing assistant/result/system events to callbacks"
|
||||||
|
artifacts:
|
||||||
|
- path: "telegram/claude_subprocess.py"
|
||||||
|
provides: "Claude Code subprocess lifecycle management"
|
||||||
|
min_lines: 120
|
||||||
|
contains: "class ClaudeSubprocess"
|
||||||
|
key_links:
|
||||||
|
- from: "telegram/claude_subprocess.py"
|
||||||
|
to: "claude CLI"
|
||||||
|
via: "asyncio.create_subprocess_exec with PIPE"
|
||||||
|
pattern: "create_subprocess_exec.*claude"
|
||||||
|
- from: "telegram/claude_subprocess.py"
|
||||||
|
to: "asyncio.gather"
|
||||||
|
via: "concurrent stdout/stderr reading"
|
||||||
|
pattern: "asyncio\\.gather"
|
||||||
|
- from: "telegram/claude_subprocess.py"
|
||||||
|
to: "process cleanup"
|
||||||
|
via: "terminate + wait_for + kill fallback"
|
||||||
|
pattern: "terminate.*wait_for|kill.*wait"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create the Claude Code subprocess engine that safely spawns, communicates with, and manages Claude Code CLI processes using asyncio.
|
||||||
|
|
||||||
|
Purpose: This module is the I/O bridge between session management and Claude Code. It handles the dangerous parts: pipe management without deadlocks, process lifecycle without zombies, message queueing during processing, and crash recovery with session resumption. The research (01-RESEARCH.md) has validated that pipes + stream-json is the correct approach over PTY.
|
||||||
|
|
||||||
|
Output: `telegram/claude_subprocess.py` module with `ClaudeSubprocess` class.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@/home/mikkel/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@/home/mikkel/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/01-session-process-foundation/01-CONTEXT.md
|
||||||
|
@.planning/phases/01-session-process-foundation/01-RESEARCH.md
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create ClaudeSubprocess module with spawn, I/O, and lifecycle management</name>
|
||||||
|
<files>telegram/claude_subprocess.py</files>
|
||||||
|
<action>
|
||||||
|
Create `telegram/claude_subprocess.py` with a `ClaudeSubprocess` class that manages a single Claude Code CLI subprocess.
|
||||||
|
|
||||||
|
**Class design:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ClaudeSubprocess:
|
||||||
|
def __init__(self, session_dir: Path, persona: dict = None,
|
||||||
|
on_output: Callable = None, on_error: Callable = None,
|
||||||
|
on_complete: Callable = None, on_status: Callable = None):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
session_dir: Path to session directory (cwd for subprocess)
|
||||||
|
persona: Persona dict with system_prompt and settings
|
||||||
|
on_output: Callback(text: str) for assistant text output
|
||||||
|
on_error: Callback(error: str) for error messages
|
||||||
|
on_complete: Callback() when a turn completes
|
||||||
|
on_status: Callback(status: str) for status updates (e.g. "Claude restarted")
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required methods:**
|
||||||
|
|
||||||
|
1. `async send_message(message: str) -> None`
|
||||||
|
- If no process is running: spawn one with this message
|
||||||
|
- If process IS running and BUSY: queue message (append to internal asyncio.Queue)
|
||||||
|
- If process IS running and IDLE: send as new turn (spawn new `claude -p` invocation)
|
||||||
|
- Track processing state (busy vs idle) to know when to queue
|
||||||
|
|
||||||
|
2. `async _spawn(message: str) -> None`
|
||||||
|
- Build Claude Code command:
|
||||||
|
```
|
||||||
|
claude -p "<message>"
|
||||||
|
--output-format stream-json
|
||||||
|
--verbose
|
||||||
|
--max-turns <from persona settings, default 25>
|
||||||
|
--model <from persona settings, default claude-sonnet-4-20250514>
|
||||||
|
```
|
||||||
|
- If persona has system_prompt, add: `--system-prompt "<system_prompt>"`
|
||||||
|
- If `.claude/` exists in session_dir (prior session): add `--continue` flag for history
|
||||||
|
- Spawn with `asyncio.create_subprocess_exec()`:
|
||||||
|
- stdout=PIPE, stderr=PIPE
|
||||||
|
- cwd=str(session_dir)
|
||||||
|
- env: inherit current env, ensure PATH includes ~/bin and ~/.local/bin
|
||||||
|
- Store process reference in `self._process`
|
||||||
|
- Store PID for metadata updates
|
||||||
|
- Set `self._busy = True`
|
||||||
|
- Launch concurrent stream readers via `asyncio.create_task(self._read_streams())`
|
||||||
|
|
||||||
|
3. `async _read_streams() -> None`
|
||||||
|
- Use `asyncio.gather()` to read stdout and stderr concurrently (CRITICAL for deadlock prevention)
|
||||||
|
- stdout handler: `self._handle_stdout_line(line)`
|
||||||
|
- stderr handler: `self._handle_stderr_line(line)`
|
||||||
|
- After both streams end: `await self._process.wait()`
|
||||||
|
- Set `self._busy = False`
|
||||||
|
- Call `self.on_complete()` callback
|
||||||
|
- Process queued messages: if `self._message_queue` not empty, pop and `await self.send_message(msg)`
|
||||||
|
|
||||||
|
4. `_handle_stdout_line(line: str) -> None`
|
||||||
|
- Parse as JSON (stream-json format, one JSON object per line)
|
||||||
|
- Route by event type:
|
||||||
|
- `"assistant"`: Extract text blocks from `event["message"]["content"]`, call `self.on_output(text)` for each text block
|
||||||
|
- `"result"`: Turn complete. If `event.get("is_error")`, call `self.on_error(...)`. Log session_id if present.
|
||||||
|
- `"system"`: Log system events. If subtype is error, call `self.on_error(...)`.
|
||||||
|
- On `json.JSONDecodeError`: log warning, skip line (Claude Code may emit non-JSON lines)
|
||||||
|
|
||||||
|
5. `_handle_stderr_line(line: str) -> None`
|
||||||
|
- Log as warning (stderr from Claude Code is usually diagnostics, not errors)
|
||||||
|
- If line contains "error" (case-insensitive), also call `self.on_error(line)`
|
||||||
|
|
||||||
|
6. `async terminate(timeout: int = 10) -> None`
|
||||||
|
- If no process or already terminated (`returncode is not None`): return
|
||||||
|
- Call `self._process.terminate()` (SIGTERM)
|
||||||
|
- `await asyncio.wait_for(self._process.wait(), timeout=timeout)`
|
||||||
|
- On TimeoutError: `self._process.kill()` then `await self._process.wait()` (CRITICAL: always reap)
|
||||||
|
- Clear `self._process` reference
|
||||||
|
- Set `self._busy = False`
|
||||||
|
|
||||||
|
7. `async _handle_crash() -> None`
|
||||||
|
- Called when process exits with non-zero return code unexpectedly
|
||||||
|
- Call `self.on_status("Claude crashed, restarting with context preserved...")` if callback set
|
||||||
|
- Wait 1 second (backoff)
|
||||||
|
- Respawn with `--continue` flag (loads most recent session from .claude/ in session_dir)
|
||||||
|
- If respawn fails 3 times: call `self.on_error("Claude failed to restart after 3 attempts")`
|
||||||
|
|
||||||
|
8. `@property is_busy -> bool`
|
||||||
|
- Return whether subprocess is currently processing a message
|
||||||
|
|
||||||
|
9. `@property is_alive -> bool`
|
||||||
|
- Return whether subprocess process is running (process exists and returncode is None)
|
||||||
|
|
||||||
|
**Internal state:**
|
||||||
|
- `self._process: asyncio.subprocess.Process | None`
|
||||||
|
- `self._busy: bool`
|
||||||
|
- `self._message_queue: asyncio.Queue`
|
||||||
|
- `self._reader_task: asyncio.Task | None`
|
||||||
|
- `self._crash_count: int` (reset on successful completion)
|
||||||
|
- `self._session_dir: Path`
|
||||||
|
- `self._persona: dict | None`
|
||||||
|
|
||||||
|
**Implementation notes:**
|
||||||
|
- Use `asyncio.create_subprocess_exec` (NOT `shell=True` -- avoid shell injection)
|
||||||
|
- For the env, ensure PATH includes `/home/mikkel/bin:/home/mikkel/.local/bin`
|
||||||
|
- Add `logging` with `logging.getLogger(__name__)`
|
||||||
|
- Include type hints for all methods
|
||||||
|
- Add module docstring explaining the subprocess interaction model
|
||||||
|
- The `_read_streams` method must handle the case where stdout/stderr complete at different times
|
||||||
|
- Use `async for line in stream` pattern or `readline()` loop for line-by-line reading
|
||||||
|
|
||||||
|
**Read 01-RESEARCH.md** for verified code patterns (Pattern 1: Concurrent Stream Reading, Pattern 3: Stream-JSON Event Handling, Pattern 4: Process Lifecycle Management).
|
||||||
|
|
||||||
|
**DO NOT:**
|
||||||
|
- Import session_manager.py (that module manages metadata; this module manages processes)
|
||||||
|
- Add Telegram-specific imports (that's Plan 03)
|
||||||
|
- Implement idle timeout (that's Phase 3)
|
||||||
|
- Use PTY (research confirms pipes are correct for Claude Code CLI)
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
```bash
|
||||||
|
cd ~/homelab
|
||||||
|
source ~/venv/bin/activate
|
||||||
|
|
||||||
|
# Verify module loads and class structure is correct
|
||||||
|
python3 -c "
|
||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
from telegram.claude_subprocess import ClaudeSubprocess
|
||||||
|
|
||||||
|
# Verify class exists and has required methods
|
||||||
|
sub = ClaudeSubprocess(
|
||||||
|
session_dir=Path('/tmp/test-claude-session'),
|
||||||
|
on_output=lambda text: print(f'OUTPUT: {text}'),
|
||||||
|
on_error=lambda err: print(f'ERROR: {err}'),
|
||||||
|
on_complete=lambda: print('COMPLETE'),
|
||||||
|
on_status=lambda s: print(f'STATUS: {s}')
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hasattr(sub, 'send_message'), 'missing send_message'
|
||||||
|
assert hasattr(sub, 'terminate'), 'missing terminate'
|
||||||
|
assert hasattr(sub, 'is_busy'), 'missing is_busy'
|
||||||
|
assert hasattr(sub, 'is_alive'), 'missing is_alive'
|
||||||
|
assert not sub.is_busy, 'should start not busy'
|
||||||
|
assert not sub.is_alive, 'should start not alive'
|
||||||
|
|
||||||
|
print('ClaudeSubprocess class structure verified!')
|
||||||
|
"
|
||||||
|
|
||||||
|
# Verify concurrent stream reading implementation exists
|
||||||
|
python3 -c "
|
||||||
|
import inspect
|
||||||
|
from telegram.claude_subprocess import ClaudeSubprocess
|
||||||
|
source = inspect.getsource(ClaudeSubprocess)
|
||||||
|
assert 'asyncio.gather' in source, 'Missing asyncio.gather for concurrent stream reading'
|
||||||
|
assert 'create_subprocess_exec' in source, 'Missing create_subprocess_exec'
|
||||||
|
assert 'stream-json' in source, 'Missing stream-json output format'
|
||||||
|
assert 'terminate' in source, 'Missing terminate method'
|
||||||
|
assert 'wait_for' in source or 'wait(' in source, 'Missing process wait'
|
||||||
|
print('Implementation patterns verified!')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
</verify>
|
||||||
|
<done>ClaudeSubprocess class spawns Claude Code CLI with stream-json output in session directories, reads stdout/stderr concurrently via asyncio.gather, handles process lifecycle with clean termination (no zombies), queues messages during processing, and auto-restarts on crash with --continue flag.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. `telegram/claude_subprocess.py` exists with `ClaudeSubprocess` class
|
||||||
|
2. Class uses `asyncio.create_subprocess_exec` (not shell=True)
|
||||||
|
3. Stdout and stderr reading uses `asyncio.gather` for concurrent draining
|
||||||
|
4. Process termination implements terminate -> wait_for -> kill -> wait pattern
|
||||||
|
5. Message queue uses `asyncio.Queue` for thread-safe queueing
|
||||||
|
6. Crash recovery attempts respawn with `--continue` flag, max 3 retries
|
||||||
|
7. Stream-json parsing handles assistant, result, and system event types
|
||||||
|
8. No imports from session_manager or telegram bot modules
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- ClaudeSubprocess module loads without import errors
|
||||||
|
- Class has all required methods and properties
|
||||||
|
- Implementation uses asyncio.gather for concurrent stream reading (verified via source inspection)
|
||||||
|
- Process lifecycle follows terminate -> wait pattern (verified via source inspection)
|
||||||
|
- Module is self-contained with callback-based communication (no tight coupling to session manager or bot)
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/01-session-process-foundation/01-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
269
.planning/phases/01-session-process-foundation/01-03-PLAN.md
Normal file
269
.planning/phases/01-session-process-foundation/01-03-PLAN.md
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
---
|
||||||
|
phase: 01-session-process-foundation
|
||||||
|
plan: 03
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on: ["01-01", "01-02"]
|
||||||
|
files_modified:
|
||||||
|
- telegram/bot.py
|
||||||
|
autonomous: false
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "User sends /new myproject in Telegram and receives confirmation that session was created"
|
||||||
|
- "User sends /new myproject brainstorm and session is created with brainstorm persona"
|
||||||
|
- "User sends /session myproject in Telegram and active session switches"
|
||||||
|
- "User sends plain text with no active session and gets prompted to create one"
|
||||||
|
- "User sends plain text with active session and message is routed to ClaudeSubprocess"
|
||||||
|
- "/new with duplicate name returns friendly error, not crash"
|
||||||
|
- "No zombie processes after switching sessions"
|
||||||
|
artifacts:
|
||||||
|
- path: "telegram/bot.py"
|
||||||
|
provides: "Bot with /new and /session commands wired to session manager and subprocess"
|
||||||
|
contains: "SessionManager"
|
||||||
|
key_links:
|
||||||
|
- from: "telegram/bot.py"
|
||||||
|
to: "telegram/session_manager.py"
|
||||||
|
via: "SessionManager import and method calls"
|
||||||
|
pattern: "from telegram\\.session_manager import|SessionManager"
|
||||||
|
- from: "telegram/bot.py"
|
||||||
|
to: "telegram/claude_subprocess.py"
|
||||||
|
via: "ClaudeSubprocess import for process spawning"
|
||||||
|
pattern: "from telegram\\.claude_subprocess import|ClaudeSubprocess"
|
||||||
|
- from: "telegram/bot.py /new handler"
|
||||||
|
to: "SessionManager.create_session"
|
||||||
|
via: "direct method call"
|
||||||
|
pattern: "create_session"
|
||||||
|
- from: "telegram/bot.py /session handler"
|
||||||
|
to: "SessionManager.switch_session"
|
||||||
|
via: "direct method call"
|
||||||
|
pattern: "switch_session"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Wire the session manager and Claude subprocess modules into the existing Telegram bot, adding `/new` and `/session` commands and routing plain text messages to the active session's Claude Code subprocess.
|
||||||
|
|
||||||
|
Purpose: This plan connects the foundation pieces (Plans 01 and 02) to the existing Telegram bot. After this plan, users can create sessions, switch between them, and send messages that spawn Claude Code subprocesses. This completes Phase 1's core goal: path-based sessions with subprocess management.
|
||||||
|
|
||||||
|
Output: Updated `telegram/bot.py` with new command handlers and session-aware message routing.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@/home/mikkel/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@/home/mikkel/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/01-session-process-foundation/01-CONTEXT.md
|
||||||
|
@.planning/phases/01-session-process-foundation/01-RESEARCH.md
|
||||||
|
@.planning/phases/01-session-process-foundation/01-01-SUMMARY.md
|
||||||
|
@.planning/phases/01-session-process-foundation/01-02-SUMMARY.md
|
||||||
|
@telegram/bot.py
|
||||||
|
@telegram/session_manager.py
|
||||||
|
@telegram/claude_subprocess.py
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add /new and /session commands to bot.py</name>
|
||||||
|
<files>telegram/bot.py</files>
|
||||||
|
<action>
|
||||||
|
Modify the existing `telegram/bot.py` to add session management commands and wire up the Claude subprocess for message handling.
|
||||||
|
|
||||||
|
**New imports to add:**
|
||||||
|
```python
|
||||||
|
from telegram.session_manager import SessionManager
|
||||||
|
from telegram.claude_subprocess import ClaudeSubprocess
|
||||||
|
```
|
||||||
|
|
||||||
|
**Module-level state:**
|
||||||
|
- Create a `SessionManager` instance (singleton for the bot process)
|
||||||
|
- Create a dict `subprocesses: dict[str, ClaudeSubprocess]` to track active subprocess per session
|
||||||
|
- Track `active_session: str | None` at module level (or via SessionManager)
|
||||||
|
|
||||||
|
**New command: `/new <name> [persona]`**
|
||||||
|
|
||||||
|
Handler: `async def new_session(update, context)`
|
||||||
|
- Extract name from `context.args[0]` (required)
|
||||||
|
- Extract persona from `context.args[1]` if provided (optional)
|
||||||
|
- Validate: if no args, reply with usage: "Usage: /new <name> [persona]"
|
||||||
|
- Call `session_manager.create_session(name, persona=persona)`
|
||||||
|
- On ValueError (duplicate name): reply "Session '<name>' already exists. Use /session <name> to switch to it."
|
||||||
|
- On success:
|
||||||
|
- Auto-switch to new session: call `session_manager.switch_session(name)`
|
||||||
|
- Reply "Session '<name>' created." (include persona name if specified: "Session '<name>' created with persona '<persona>'.")
|
||||||
|
- Do NOT auto-spawn subprocess yet (spawn happens on first message, per CONTEXT.md: "Switching to a session with no running process auto-spawns Claude Code immediately" -- but creating is not switching. Actually, /new does auto-switch, so it should auto-spawn. However, we can defer spawn to first message for simplicity. Let Claude handle this: spawn subprocess immediately after creation since we auto-switch.)
|
||||||
|
- Spawn a ClaudeSubprocess for the new session (but don't send a message yet -- it's just ready)
|
||||||
|
|
||||||
|
**New command: `/session <name>`**
|
||||||
|
|
||||||
|
Handler: `async def switch_session_cmd(update, context)`
|
||||||
|
- Extract name from `context.args[0]` (required)
|
||||||
|
- Validate: if no args, reply with usage and list available sessions
|
||||||
|
- If session doesn't exist: reply "Session '<name>' doesn't exist. Use /new <name> to create it."
|
||||||
|
- Call `session_manager.switch_session(name)`
|
||||||
|
- If session has no running subprocess: spawn one (create ClaudeSubprocess, don't send message)
|
||||||
|
- Reply "Switched to session '<name>'." (include persona if set)
|
||||||
|
|
||||||
|
**Modified message handler: `handle_message`**
|
||||||
|
|
||||||
|
Replace the current implementation (which saves to inbox) with session-aware routing:
|
||||||
|
- If no active session: reply "No active session. Use /new <name> to start one."
|
||||||
|
- If active session exists:
|
||||||
|
- Get or create ClaudeSubprocess for session
|
||||||
|
- Call `await subprocess.send_message(update.message.text)`
|
||||||
|
- The on_output callback will need to send messages back to the chat. For Phase 1, create callbacks that send to the Telegram chat:
|
||||||
|
```python
|
||||||
|
async def on_output(text):
|
||||||
|
await update.message.reply_text(text)
|
||||||
|
```
|
||||||
|
- Note: For Phase 1, basic output routing is needed. Full Telegram integration (typing indicators, message batching, code block handling) comes in Phase 2.
|
||||||
|
- Register handler with `block=False` for non-blocking execution (per research: prevents blocking Telegram event loop)
|
||||||
|
|
||||||
|
**Subprocess callback factory:**
|
||||||
|
|
||||||
|
Create a helper function that generates callbacks bound to a specific chat_id and bot instance:
|
||||||
|
```python
|
||||||
|
def make_callbacks(bot, chat_id):
|
||||||
|
async def on_output(text):
|
||||||
|
# Truncate to Telegram limit
|
||||||
|
if len(text) > 4000:
|
||||||
|
text = text[:4000] + "\n... (truncated)"
|
||||||
|
await bot.send_message(chat_id=chat_id, text=text)
|
||||||
|
|
||||||
|
async def on_error(error):
|
||||||
|
await bot.send_message(chat_id=chat_id, text=f"Error: {error}")
|
||||||
|
|
||||||
|
async def on_complete():
|
||||||
|
pass # Phase 2 will add typing indicator cleanup
|
||||||
|
|
||||||
|
async def on_status(status):
|
||||||
|
await bot.send_message(chat_id=chat_id, text=f"[{status}]")
|
||||||
|
|
||||||
|
return on_output, on_error, on_complete, on_status
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important: callback async handling.** The ClaudeSubprocess callbacks may be called from a background task. Since `bot.send_message` is async, the callbacks need to be async and properly awaited. The subprocess module should use `asyncio.ensure_future()` or check if callbacks are coroutines.
|
||||||
|
|
||||||
|
Actually, reconsider: the subprocess stream reader runs in a background asyncio.Task. Callbacks called from there ARE in the event loop already. So async callbacks work naturally. The subprocess module should `await callback(text)` if the callback is a coroutine, or call `asyncio.create_task(callback(text))` to not block the reader.
|
||||||
|
|
||||||
|
**Handler registration updates:**
|
||||||
|
|
||||||
|
Add to `main()`:
|
||||||
|
```python
|
||||||
|
app.add_handler(CommandHandler("new", new_session))
|
||||||
|
app.add_handler(CommandHandler("session", switch_session_cmd))
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the text message handler to use `block=False`:
|
||||||
|
```python
|
||||||
|
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message, block=False))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update /help text:** Add /new and /session to the help output.
|
||||||
|
|
||||||
|
**Keep all existing commands working.** Do not remove or modify `/status`, `/pbs`, `/backups`, etc. Only modify `handle_message` and add new handlers.
|
||||||
|
|
||||||
|
**Authorization:** All new handlers must check `is_authorized(update.effective_user.id)` as the first line, following existing pattern.
|
||||||
|
|
||||||
|
**DO NOT:**
|
||||||
|
- Remove or break existing command handlers
|
||||||
|
- Implement typing indicators (Phase 2)
|
||||||
|
- Implement message batching or code block handling (Phase 2)
|
||||||
|
- Implement idle timeout (Phase 3)
|
||||||
|
- Make the bot.py import path different from existing pattern
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
```bash
|
||||||
|
cd ~/homelab
|
||||||
|
source ~/venv/bin/activate
|
||||||
|
|
||||||
|
# Verify bot.py loads without import errors
|
||||||
|
python3 -c "
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '.')
|
||||||
|
# Just verify imports work - don't run the bot
|
||||||
|
from telegram.session_manager import SessionManager
|
||||||
|
from telegram.claude_subprocess import ClaudeSubprocess
|
||||||
|
print('Imports successful')
|
||||||
|
"
|
||||||
|
|
||||||
|
# Verify new handlers are registered in the code
|
||||||
|
python3 -c "
|
||||||
|
source = open('telegram/bot.py').read()
|
||||||
|
assert 'CommandHandler(\"new\"' in source or 'CommandHandler(\"new\",' in source, 'Missing /new handler registration'
|
||||||
|
assert 'CommandHandler(\"session\"' in source or 'CommandHandler(\"session\",' in source, 'Missing /session handler registration'
|
||||||
|
assert 'SessionManager' in source, 'Missing SessionManager usage'
|
||||||
|
assert 'ClaudeSubprocess' in source, 'Missing ClaudeSubprocess usage'
|
||||||
|
assert 'block=False' in source, 'Missing block=False for non-blocking handler'
|
||||||
|
assert 'is_authorized' in source, 'Authorization checks must be present'
|
||||||
|
print('Handler registration and imports verified!')
|
||||||
|
"
|
||||||
|
|
||||||
|
# Verify existing handlers still present
|
||||||
|
python3 -c "
|
||||||
|
source = open('telegram/bot.py').read()
|
||||||
|
existing = ['status', 'pbs', 'backups', 'beszel', 'kuma', 'ping', 'help']
|
||||||
|
for cmd in existing:
|
||||||
|
assert f'CommandHandler(\"{cmd}\"' in source or f'CommandHandler(\"{cmd}\",' in source, f'Existing /{cmd} handler missing!'
|
||||||
|
print('All existing handlers preserved!')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
</verify>
|
||||||
|
<done>/new and /session commands are registered in bot.py, SessionManager and ClaudeSubprocess are wired in, plain text messages route to active session's subprocess (or prompt if no session), all existing bot commands still work, handlers use block=False for non-blocking execution.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="checkpoint:human-verify" gate="blocking">
|
||||||
|
<name>Task 2: Verify session commands work in Telegram</name>
|
||||||
|
<what-built>
|
||||||
|
Session management commands (/new, /session) integrated into the Telegram bot, with Claude Code subprocess spawning and basic message routing. The bot should handle session creation, switching, and message forwarding to Claude Code.
|
||||||
|
</what-built>
|
||||||
|
<how-to-verify>
|
||||||
|
1. Restart the Telegram bot service:
|
||||||
|
```
|
||||||
|
systemctl --user restart telegram-bot.service
|
||||||
|
systemctl --user status telegram-bot.service
|
||||||
|
```
|
||||||
|
2. In Telegram, send `/new test-session` -- expect confirmation "Session 'test-session' created."
|
||||||
|
3. Send a plain text message like "Hello, what can you do?" -- expect Claude's response back in Telegram
|
||||||
|
4. Send `/new second brainstorm` -- expect "Session 'second' created with persona 'brainstorm'."
|
||||||
|
5. Send `/session test-session` -- expect "Switched to session 'test-session'."
|
||||||
|
6. Send another message -- should go to test-session's Claude (different context from second)
|
||||||
|
7. Check for zombie processes: `ps aux | grep defunct` -- should be empty
|
||||||
|
8. Check session directories exist: `ls ~/telegram/sessions/` -- should show test-session and second
|
||||||
|
9. Send `/new test-session` (duplicate) -- expect friendly error, not crash
|
||||||
|
10. Send a message without active session (if possible to test): should get "No active session" prompt
|
||||||
|
</how-to-verify>
|
||||||
|
<resume-signal>Type "approved" if sessions work correctly, or describe any issues found.</resume-signal>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. Bot starts without errors after code changes
|
||||||
|
2. `/new <name>` creates session directory with metadata.json and persona.json
|
||||||
|
3. `/session <name>` switches active session
|
||||||
|
4. Plain text messages route to active session's Claude Code subprocess
|
||||||
|
5. Claude Code responses come back to Telegram chat
|
||||||
|
6. All existing commands (/status, /pbs, /backups, etc.) still work
|
||||||
|
7. No zombie processes after session switches
|
||||||
|
8. Duplicate /new returns friendly error
|
||||||
|
9. Message with no active session prompts user
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- `/new test` creates session and confirms in Telegram
|
||||||
|
- `/session test` switches session and confirms
|
||||||
|
- Plain text messages trigger Claude Code and response appears in Telegram
|
||||||
|
- Existing bot commands remain functional
|
||||||
|
- No zombie processes (verified via `ps aux | grep defunct`)
|
||||||
|
- Bot service runs stably under systemd
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/01-session-process-foundation/01-03-SUMMARY.md`
|
||||||
|
</output>
|
||||||
Loading…
Add table
Reference in a new issue