mirror of
https://github.com/K-Dense-AI/claude-scientific-skills.git
synced 2026-03-27 07:09:27 +08:00
Add planning with files skill from @OthmanAdi
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
# Check if all phases in task_plan.md are complete
|
||||
# Always exits 0 -- uses stdout for status reporting
|
||||
# Used by Stop hook to report task completion status
|
||||
|
||||
param(
|
||||
[string]$PlanFile = "task_plan.md"
|
||||
)
|
||||
|
||||
if (-not (Test-Path $PlanFile)) {
|
||||
Write-Host '[planning-with-files] No task_plan.md found -- no active planning session.'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Read file content
|
||||
$content = Get-Content $PlanFile -Raw
|
||||
|
||||
# Count total phases
|
||||
$TOTAL = ([regex]::Matches($content, "### Phase")).Count
|
||||
|
||||
# Check for **Status:** format first
|
||||
$COMPLETE = ([regex]::Matches($content, "\*\*Status:\*\* complete")).Count
|
||||
$IN_PROGRESS = ([regex]::Matches($content, "\*\*Status:\*\* in_progress")).Count
|
||||
$PENDING = ([regex]::Matches($content, "\*\*Status:\*\* pending")).Count
|
||||
|
||||
# Fallback: check for [complete] inline format if **Status:** not found
|
||||
if ($COMPLETE -eq 0 -and $IN_PROGRESS -eq 0 -and $PENDING -eq 0) {
|
||||
$COMPLETE = ([regex]::Matches($content, "\[complete\]")).Count
|
||||
$IN_PROGRESS = ([regex]::Matches($content, "\[in_progress\]")).Count
|
||||
$PENDING = ([regex]::Matches($content, "\[pending\]")).Count
|
||||
}
|
||||
|
||||
# Report status -- always exit 0, incomplete task is a normal state
|
||||
if ($COMPLETE -eq $TOTAL -and $TOTAL -gt 0) {
|
||||
Write-Host ('[planning-with-files] ALL PHASES COMPLETE (' + $COMPLETE + '/' + $TOTAL + '). If the user has additional work, add new phases to task_plan.md before starting.')
|
||||
} else {
|
||||
Write-Host ('[planning-with-files] Task in progress (' + $COMPLETE + '/' + $TOTAL + ' phases complete). Update progress.md before stopping.')
|
||||
if ($IN_PROGRESS -gt 0) {
|
||||
Write-Host ('[planning-with-files] ' + $IN_PROGRESS + ' phase(s) still in progress.')
|
||||
}
|
||||
if ($PENDING -gt 0) {
|
||||
Write-Host ('[planning-with-files] ' + $PENDING + ' phase(s) pending.')
|
||||
}
|
||||
}
|
||||
exit 0
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# Check if all phases in task_plan.md are complete
|
||||
# Always exits 0 — uses stdout for status reporting
|
||||
# Used by Stop hook to report task completion status
|
||||
|
||||
PLAN_FILE="${1:-task_plan.md}"
|
||||
|
||||
if [ ! -f "$PLAN_FILE" ]; then
|
||||
echo "[planning-with-files] No task_plan.md found — no active planning session."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Count total phases
|
||||
TOTAL=$(grep -c "### Phase" "$PLAN_FILE" || true)
|
||||
|
||||
# Check for **Status:** format first
|
||||
COMPLETE=$(grep -cF "**Status:** complete" "$PLAN_FILE" || true)
|
||||
IN_PROGRESS=$(grep -cF "**Status:** in_progress" "$PLAN_FILE" || true)
|
||||
PENDING=$(grep -cF "**Status:** pending" "$PLAN_FILE" || true)
|
||||
|
||||
# Fallback: check for [complete] inline format if **Status:** not found
|
||||
if [ "$COMPLETE" -eq 0 ] && [ "$IN_PROGRESS" -eq 0 ] && [ "$PENDING" -eq 0 ]; then
|
||||
COMPLETE=$(grep -c "\[complete\]" "$PLAN_FILE" || true)
|
||||
IN_PROGRESS=$(grep -c "\[in_progress\]" "$PLAN_FILE" || true)
|
||||
PENDING=$(grep -c "\[pending\]" "$PLAN_FILE" || true)
|
||||
fi
|
||||
|
||||
# Default to 0 if empty
|
||||
: "${TOTAL:=0}"
|
||||
: "${COMPLETE:=0}"
|
||||
: "${IN_PROGRESS:=0}"
|
||||
: "${PENDING:=0}"
|
||||
|
||||
# Report status (always exit 0 — incomplete task is a normal state)
|
||||
if [ "$COMPLETE" -eq "$TOTAL" ] && [ "$TOTAL" -gt 0 ]; then
|
||||
echo "[planning-with-files] ALL PHASES COMPLETE ($COMPLETE/$TOTAL). If the user has additional work, add new phases to task_plan.md before starting."
|
||||
else
|
||||
echo "[planning-with-files] Task in progress ($COMPLETE/$TOTAL phases complete). Update progress.md before stopping."
|
||||
if [ "$IN_PROGRESS" -gt 0 ]; then
|
||||
echo "[planning-with-files] $IN_PROGRESS phase(s) still in progress."
|
||||
fi
|
||||
if [ "$PENDING" -gt 0 ]; then
|
||||
echo "[planning-with-files] $PENDING phase(s) pending."
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
120
scientific-skills/planning-with-files/scripts/init-session.ps1
Normal file
120
scientific-skills/planning-with-files/scripts/init-session.ps1
Normal file
@@ -0,0 +1,120 @@
|
||||
# Initialize planning files for a new session
|
||||
# Usage: .\init-session.ps1 [project-name]
|
||||
|
||||
param(
|
||||
[string]$ProjectName = "project"
|
||||
)
|
||||
|
||||
$DATE = Get-Date -Format "yyyy-MM-dd"
|
||||
|
||||
Write-Host "Initializing planning files for: $ProjectName"
|
||||
|
||||
# Create task_plan.md if it doesn't exist
|
||||
if (-not (Test-Path "task_plan.md")) {
|
||||
@"
|
||||
# Task Plan: [Brief Description]
|
||||
|
||||
## Goal
|
||||
[One sentence describing the end state]
|
||||
|
||||
## Current Phase
|
||||
Phase 1
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 1: Requirements & Discovery
|
||||
- [ ] Understand user intent
|
||||
- [ ] Identify constraints
|
||||
- [ ] Document in findings.md
|
||||
- **Status:** in_progress
|
||||
|
||||
### Phase 2: Planning & Structure
|
||||
- [ ] Define approach
|
||||
- [ ] Create project structure
|
||||
- **Status:** pending
|
||||
|
||||
### Phase 3: Implementation
|
||||
- [ ] Execute the plan
|
||||
- [ ] Write to files before executing
|
||||
- **Status:** pending
|
||||
|
||||
### Phase 4: Testing & Verification
|
||||
- [ ] Verify requirements met
|
||||
- [ ] Document test results
|
||||
- **Status:** pending
|
||||
|
||||
### Phase 5: Delivery
|
||||
- [ ] Review outputs
|
||||
- [ ] Deliver to user
|
||||
- **Status:** pending
|
||||
|
||||
## Decisions Made
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
|
||||
## Errors Encountered
|
||||
| Error | Resolution |
|
||||
|-------|------------|
|
||||
"@ | Out-File -FilePath "task_plan.md" -Encoding UTF8
|
||||
Write-Host "Created task_plan.md"
|
||||
} else {
|
||||
Write-Host "task_plan.md already exists, skipping"
|
||||
}
|
||||
|
||||
# Create findings.md if it doesn't exist
|
||||
if (-not (Test-Path "findings.md")) {
|
||||
@"
|
||||
# Findings & Decisions
|
||||
|
||||
## Requirements
|
||||
-
|
||||
|
||||
## Research Findings
|
||||
-
|
||||
|
||||
## Technical Decisions
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
|
||||
## Issues Encountered
|
||||
| Issue | Resolution |
|
||||
|-------|------------|
|
||||
|
||||
## Resources
|
||||
-
|
||||
"@ | Out-File -FilePath "findings.md" -Encoding UTF8
|
||||
Write-Host "Created findings.md"
|
||||
} else {
|
||||
Write-Host "findings.md already exists, skipping"
|
||||
}
|
||||
|
||||
# Create progress.md if it doesn't exist
|
||||
if (-not (Test-Path "progress.md")) {
|
||||
@"
|
||||
# Progress Log
|
||||
|
||||
## Session: $DATE
|
||||
|
||||
### Current Status
|
||||
- **Phase:** 1 - Requirements & Discovery
|
||||
- **Started:** $DATE
|
||||
|
||||
### Actions Taken
|
||||
-
|
||||
|
||||
### Test Results
|
||||
| Test | Expected | Actual | Status |
|
||||
|------|----------|--------|--------|
|
||||
|
||||
### Errors
|
||||
| Error | Resolution |
|
||||
|-------|------------|
|
||||
"@ | Out-File -FilePath "progress.md" -Encoding UTF8
|
||||
Write-Host "Created progress.md"
|
||||
} else {
|
||||
Write-Host "progress.md already exists, skipping"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Planning files initialized!"
|
||||
Write-Host "Files: task_plan.md, findings.md, progress.md"
|
||||
120
scientific-skills/planning-with-files/scripts/init-session.sh
Normal file
120
scientific-skills/planning-with-files/scripts/init-session.sh
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/bin/bash
|
||||
# Initialize planning files for a new session
|
||||
# Usage: ./init-session.sh [project-name]
|
||||
|
||||
set -e
|
||||
|
||||
PROJECT_NAME="${1:-project}"
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
|
||||
echo "Initializing planning files for: $PROJECT_NAME"
|
||||
|
||||
# Create task_plan.md if it doesn't exist
|
||||
if [ ! -f "task_plan.md" ]; then
|
||||
cat > task_plan.md << 'EOF'
|
||||
# Task Plan: [Brief Description]
|
||||
|
||||
## Goal
|
||||
[One sentence describing the end state]
|
||||
|
||||
## Current Phase
|
||||
Phase 1
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 1: Requirements & Discovery
|
||||
- [ ] Understand user intent
|
||||
- [ ] Identify constraints
|
||||
- [ ] Document in findings.md
|
||||
- **Status:** in_progress
|
||||
|
||||
### Phase 2: Planning & Structure
|
||||
- [ ] Define approach
|
||||
- [ ] Create project structure
|
||||
- **Status:** pending
|
||||
|
||||
### Phase 3: Implementation
|
||||
- [ ] Execute the plan
|
||||
- [ ] Write to files before executing
|
||||
- **Status:** pending
|
||||
|
||||
### Phase 4: Testing & Verification
|
||||
- [ ] Verify requirements met
|
||||
- [ ] Document test results
|
||||
- **Status:** pending
|
||||
|
||||
### Phase 5: Delivery
|
||||
- [ ] Review outputs
|
||||
- [ ] Deliver to user
|
||||
- **Status:** pending
|
||||
|
||||
## Decisions Made
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
|
||||
## Errors Encountered
|
||||
| Error | Resolution |
|
||||
|-------|------------|
|
||||
EOF
|
||||
echo "Created task_plan.md"
|
||||
else
|
||||
echo "task_plan.md already exists, skipping"
|
||||
fi
|
||||
|
||||
# Create findings.md if it doesn't exist
|
||||
if [ ! -f "findings.md" ]; then
|
||||
cat > findings.md << 'EOF'
|
||||
# Findings & Decisions
|
||||
|
||||
## Requirements
|
||||
-
|
||||
|
||||
## Research Findings
|
||||
-
|
||||
|
||||
## Technical Decisions
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
|
||||
## Issues Encountered
|
||||
| Issue | Resolution |
|
||||
|-------|------------|
|
||||
|
||||
## Resources
|
||||
-
|
||||
EOF
|
||||
echo "Created findings.md"
|
||||
else
|
||||
echo "findings.md already exists, skipping"
|
||||
fi
|
||||
|
||||
# Create progress.md if it doesn't exist
|
||||
if [ ! -f "progress.md" ]; then
|
||||
cat > progress.md << EOF
|
||||
# Progress Log
|
||||
|
||||
## Session: $DATE
|
||||
|
||||
### Current Status
|
||||
- **Phase:** 1 - Requirements & Discovery
|
||||
- **Started:** $DATE
|
||||
|
||||
### Actions Taken
|
||||
-
|
||||
|
||||
### Test Results
|
||||
| Test | Expected | Actual | Status |
|
||||
|------|----------|--------|--------|
|
||||
|
||||
### Errors
|
||||
| Error | Resolution |
|
||||
|-------|------------|
|
||||
EOF
|
||||
echo "Created progress.md"
|
||||
else
|
||||
echo "progress.md already exists, skipping"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Planning files initialized!"
|
||||
echo "Files: task_plan.md, findings.md, progress.md"
|
||||
256
scientific-skills/planning-with-files/scripts/session-catchup.py
Executable file
256
scientific-skills/planning-with-files/scripts/session-catchup.py
Executable file
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Session Catchup Script for planning-with-files
|
||||
|
||||
Analyzes the previous session to find unsynced context after the last
|
||||
planning file update. Designed to run on SessionStart.
|
||||
|
||||
Usage: python3 session-catchup.py [project-path]
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
|
||||
PLANNING_FILES = ['task_plan.md', 'progress.md', 'findings.md']
|
||||
|
||||
|
||||
def normalize_path(project_path: str) -> str:
|
||||
"""Normalize project path to match Claude Code's internal representation.
|
||||
|
||||
Claude Code stores session directories using the Windows-native path
|
||||
(e.g., C:\\Users\\...) sanitized with separators replaced by dashes.
|
||||
Git Bash passes /c/Users/... which produces a DIFFERENT sanitized
|
||||
string. This function converts Git Bash paths to Windows paths first.
|
||||
"""
|
||||
p = project_path
|
||||
|
||||
# Git Bash / MSYS2: /c/Users/... -> C:/Users/...
|
||||
if len(p) >= 3 and p[0] == '/' and p[2] == '/':
|
||||
p = p[1].upper() + ':' + p[2:]
|
||||
|
||||
# Resolve to absolute path to handle relative paths and symlinks
|
||||
try:
|
||||
resolved = str(Path(p).resolve())
|
||||
# On Windows, resolve() returns C:\Users\... which is what we want
|
||||
if os.name == 'nt' or '\\' in resolved:
|
||||
p = resolved
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def get_project_dir(project_path: str) -> Tuple[Optional[Path], Optional[str]]:
|
||||
"""Resolve session storage path for the current runtime variant."""
|
||||
normalized = normalize_path(project_path)
|
||||
|
||||
# Claude Code's sanitization: replace path separators and : with -
|
||||
sanitized = normalized.replace('\\', '-').replace('/', '-').replace(':', '-')
|
||||
sanitized = sanitized.replace('_', '-')
|
||||
# Strip leading dash if present (Unix absolute paths start with /)
|
||||
if sanitized.startswith('-'):
|
||||
sanitized = sanitized[1:]
|
||||
|
||||
claude_path = Path.home() / '.claude' / 'projects' / sanitized
|
||||
|
||||
# Codex stores sessions in ~/.codex/sessions with a different format.
|
||||
# Avoid silently scanning Claude paths when running from Codex skill folder.
|
||||
script_path = Path(__file__).as_posix().lower()
|
||||
is_codex_variant = '/.codex/' in script_path
|
||||
codex_sessions_dir = Path.home() / '.codex' / 'sessions'
|
||||
if is_codex_variant and codex_sessions_dir.exists() and not claude_path.exists():
|
||||
return None, (
|
||||
"[planning-with-files] Session catchup skipped: Codex stores sessions "
|
||||
"in ~/.codex/sessions and native Codex parsing is not implemented yet."
|
||||
)
|
||||
|
||||
return claude_path, None
|
||||
|
||||
|
||||
def get_sessions_sorted(project_dir: Path) -> List[Path]:
|
||||
"""Get all session files sorted by modification time (newest first)."""
|
||||
sessions = list(project_dir.glob('*.jsonl'))
|
||||
main_sessions = [s for s in sessions if not s.name.startswith('agent-')]
|
||||
return sorted(main_sessions, key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
|
||||
|
||||
def parse_session_messages(session_file: Path) -> List[Dict]:
|
||||
"""Parse all messages from a session file, preserving order."""
|
||||
messages = []
|
||||
with open(session_file, 'r', encoding='utf-8', errors='replace') as f:
|
||||
for line_num, line in enumerate(f):
|
||||
try:
|
||||
data = json.loads(line)
|
||||
data['_line_num'] = line_num
|
||||
messages.append(data)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return messages
|
||||
|
||||
|
||||
def find_last_planning_update(messages: List[Dict]) -> Tuple[int, Optional[str]]:
|
||||
"""
|
||||
Find the last time a planning file was written/edited.
|
||||
Returns (line_number, filename) or (-1, None) if not found.
|
||||
"""
|
||||
last_update_line = -1
|
||||
last_update_file = None
|
||||
|
||||
for msg in messages:
|
||||
msg_type = msg.get('type')
|
||||
|
||||
if msg_type == 'assistant':
|
||||
content = msg.get('message', {}).get('content', [])
|
||||
if isinstance(content, list):
|
||||
for item in content:
|
||||
if item.get('type') == 'tool_use':
|
||||
tool_name = item.get('name', '')
|
||||
tool_input = item.get('input', {})
|
||||
|
||||
if tool_name in ('Write', 'Edit'):
|
||||
file_path = tool_input.get('file_path', '')
|
||||
for pf in PLANNING_FILES:
|
||||
if file_path.endswith(pf):
|
||||
last_update_line = msg['_line_num']
|
||||
last_update_file = pf
|
||||
|
||||
return last_update_line, last_update_file
|
||||
|
||||
|
||||
def extract_messages_after(messages: List[Dict], after_line: int) -> List[Dict]:
|
||||
"""Extract conversation messages after a certain line number."""
|
||||
result = []
|
||||
for msg in messages:
|
||||
if msg['_line_num'] <= after_line:
|
||||
continue
|
||||
|
||||
msg_type = msg.get('type')
|
||||
is_meta = msg.get('isMeta', False)
|
||||
|
||||
if msg_type == 'user' and not is_meta:
|
||||
content = msg.get('message', {}).get('content', '')
|
||||
if isinstance(content, list):
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get('type') == 'text':
|
||||
content = item.get('text', '')
|
||||
break
|
||||
else:
|
||||
content = ''
|
||||
|
||||
if content and isinstance(content, str):
|
||||
if content.startswith(('<local-command', '<command-', '<task-notification')):
|
||||
continue
|
||||
if len(content) > 20:
|
||||
result.append({'role': 'user', 'content': content, 'line': msg['_line_num']})
|
||||
|
||||
elif msg_type == 'assistant':
|
||||
msg_content = msg.get('message', {}).get('content', '')
|
||||
text_content = ''
|
||||
tool_uses = []
|
||||
|
||||
if isinstance(msg_content, str):
|
||||
text_content = msg_content
|
||||
elif isinstance(msg_content, list):
|
||||
for item in msg_content:
|
||||
if item.get('type') == 'text':
|
||||
text_content = item.get('text', '')
|
||||
elif item.get('type') == 'tool_use':
|
||||
tool_name = item.get('name', '')
|
||||
tool_input = item.get('input', {})
|
||||
if tool_name == 'Edit':
|
||||
tool_uses.append(f"Edit: {tool_input.get('file_path', 'unknown')}")
|
||||
elif tool_name == 'Write':
|
||||
tool_uses.append(f"Write: {tool_input.get('file_path', 'unknown')}")
|
||||
elif tool_name == 'Bash':
|
||||
cmd = tool_input.get('command', '')[:80]
|
||||
tool_uses.append(f"Bash: {cmd}")
|
||||
else:
|
||||
tool_uses.append(f"{tool_name}")
|
||||
|
||||
if text_content or tool_uses:
|
||||
result.append({
|
||||
'role': 'assistant',
|
||||
'content': text_content[:600] if text_content else '',
|
||||
'tools': tool_uses,
|
||||
'line': msg['_line_num']
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
project_path = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
|
||||
|
||||
# Check if planning files exist (indicates active task)
|
||||
has_planning_files = any(
|
||||
Path(project_path, f).exists() for f in PLANNING_FILES
|
||||
)
|
||||
if not has_planning_files:
|
||||
# No planning files in this project; skip catchup to avoid noise.
|
||||
return
|
||||
|
||||
project_dir, skip_reason = get_project_dir(project_path)
|
||||
if skip_reason:
|
||||
print(skip_reason)
|
||||
return
|
||||
|
||||
if not project_dir.exists():
|
||||
# No previous sessions, nothing to catch up on
|
||||
return
|
||||
|
||||
sessions = get_sessions_sorted(project_dir)
|
||||
if len(sessions) < 1:
|
||||
return
|
||||
|
||||
# Find a substantial previous session
|
||||
target_session = None
|
||||
for session in sessions:
|
||||
if session.stat().st_size > 5000:
|
||||
target_session = session
|
||||
break
|
||||
|
||||
if not target_session:
|
||||
return
|
||||
|
||||
messages = parse_session_messages(target_session)
|
||||
last_update_line, last_update_file = find_last_planning_update(messages)
|
||||
|
||||
# No planning updates in the target session; skip catchup output.
|
||||
if last_update_line < 0:
|
||||
return
|
||||
|
||||
# Only output if there's unsynced content
|
||||
messages_after = extract_messages_after(messages, last_update_line)
|
||||
|
||||
if not messages_after:
|
||||
return
|
||||
|
||||
# Output catchup report
|
||||
print("\n[planning-with-files] SESSION CATCHUP DETECTED")
|
||||
print(f"Previous session: {target_session.stem}")
|
||||
|
||||
print(f"Last planning update: {last_update_file} at message #{last_update_line}")
|
||||
print(f"Unsynced messages: {len(messages_after)}")
|
||||
|
||||
print("\n--- UNSYNCED CONTEXT ---")
|
||||
for msg in messages_after[-15:]: # Last 15 messages
|
||||
if msg['role'] == 'user':
|
||||
print(f"USER: {msg['content'][:300]}")
|
||||
else:
|
||||
if msg.get('content'):
|
||||
print(f"CLAUDE: {msg['content'][:300]}")
|
||||
if msg.get('tools'):
|
||||
print(f" Tools: {', '.join(msg['tools'][:4])}")
|
||||
|
||||
print("\n--- RECOMMENDED ---")
|
||||
print("1. Run: git diff --stat")
|
||||
print("2. Read: task_plan.md, progress.md, findings.md")
|
||||
print("3. Update planning files based on above context")
|
||||
print("4. Continue with task")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user