Compare commits

..
4 Commits
Author SHA1 Message Date
OthmanAdi ac76baea26 fix: SKILL.md frontmatter now complies with Agent Skills spec (fixes #39)
BREAKING CHANGE: Removed non-standard frontmatter fields

Changes:
- Removed invalid 'hooks:' field (not supported in SKILL.md per spec)
- Removed invalid top-level 'version:' (moved to metadata.version)
- Removed 'user-invocable:' field (not in official spec)
- Changed 'allowed-tools:' from YAML list to space-delimited string
- Removed ${CLAUDE_PLUGIN_ROOT} variable references

This should fix /planning-with-files slash command not appearing.

Reference: https://agentskills.io/specification
2026-01-20 10:16:02 +01:00
OthmanAdi 7de9247951 fix: replace CLAUDE_PLUGIN_ROOT with IDE-specific paths for cross-platform compatibility
Fixes #38

- Replaced ${CLAUDE_PLUGIN_ROOT} in documentation with explicit paths for Claude Code, Codex, and Cursor
- The variable works in YAML hooks but not in user-facing commands
- Users can now copy-paste the correct path for their IDE
- Maintains backward compatibility with Claude Code while fixing Codex support
2026-01-18 10:46:02 +01:00
OthmanAdi c5690231ac chore: Remove internal analysis file from repo
- Remove COMPREHENSIVE_ISSUE_ANALYSIS.md (internal notes)
- This file was accidentally committed and should not be public
2026-01-18 00:35:22 +01:00
Nicolas MaroisandClaude Opus 4.5 3237ad2763 fix: Session-agnostic catchup scanning across all sessions (#37)
The session-catchup.py script now properly scans across ALL sessions to find
unsynced context since the last planning file update.

Changes:
- Skip the current session (most recently modified)
- Find the most recent planning file update across all previous sessions
- Collect ALL conversation from that point forward, across all sessions
- Show session markers [Session: xxx...] when context spans multiple sessions
- Include intermediate sessions that have no planning updates but have work
- Show up to 100 messages (was 20), with note if truncated

Example: If planning was updated in session A, then work continued in sessions
B and C without updates, the catchup now shows context from A, B, and C.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 00:27:32 +01:00
6 changed files with 248 additions and 1517 deletions
+2 -3
View File
@@ -1,7 +1,7 @@
{
"name": "planning-with-files",
"version": "2.3.0",
"description": "Manus-style persistent markdown files for planning, progress tracking, and knowledge storage. Includes hooks for automatic plan re-reading and completion verification. Use when starting complex tasks, multi-step projects, research tasks, or when you want structured output.",
"version": "2.4.0",
"description": "Manus-style persistent markdown files for planning, progress tracking, and knowledge storage. Use when starting complex tasks, multi-step projects, research tasks, or when you want structured output.",
"author": {
"name": "OthmanAdi",
"url": "https://github.com/OthmanAdi"
@@ -15,7 +15,6 @@
"markdown",
"context-engineering",
"task-management",
"hooks",
"templates"
]
}
+46
View File
@@ -2,6 +2,52 @@
All notable changes to this project will be documented in this file.
## [2.4.0] - 2026-01-20
### Fixed
- **CRITICAL: Fixed SKILL.md frontmatter to comply with official Agent Skills spec** (Issue #39)
- Removed invalid `hooks:` field from SKILL.md frontmatter (not supported by spec)
- Removed invalid top-level `version:` field (moved to `metadata.version`)
- Removed `user-invocable:` field (not in official spec)
- Changed `allowed-tools:` from YAML list to space-delimited string per spec
- This fixes `/planning-with-files` slash command not appearing for users
### Changed
- SKILL.md frontmatter now follows [Agent Skills Specification](https://agentskills.io/specification)
- Version now stored in `metadata.version` field
- Removed `${CLAUDE_PLUGIN_ROOT}` variable references from SKILL.md (use relative paths)
- Updated plugin.json to v2.4.0
### Technical Details
The previous SKILL.md used non-standard frontmatter fields:
```yaml
# OLD (broken)
version: "2.3.0" # NOT supported at top level
user-invocable: true # NOT in official spec
hooks: # NOT supported in SKILL.md
PreToolUse: ...
```
Now uses spec-compliant format:
```yaml
# NEW (fixed)
name: planning-with-files
description: ...
license: MIT
metadata:
version: "2.4.0"
author: OthmanAdi
allowed-tools: Read Write Edit Bash Glob Grep WebFetch WebSearch
```
### Thanks
- @wqh17101 for identifying the issue in #39
- @dalisoft and @zoffyzhang for reporting the problem
## [2.3.0] - 2026-01-17
### Added
File diff suppressed because it is too large Load Diff
+10 -36
View File
@@ -1,49 +1,23 @@
---
name: planning-with-files
version: "2.3.0"
description: Implements Manus-style file-based planning for complex tasks. Creates task_plan.md, findings.md, and progress.md. Use when starting complex multi-step tasks, research projects, or any task requiring >5 tool calls. Now with automatic session recovery after /clear.
user-invocable: true
allowed-tools:
- Read
- Write
- Edit
- Bash
- Glob
- Grep
- WebFetch
- WebSearch
hooks:
PreToolUse:
- matcher: "Write|Edit|Bash|Read|Glob|Grep"
hooks:
- type: command
command: "cat task_plan.md 2>/dev/null | head -30 || true"
PostToolUse:
- matcher: "Write|Edit"
hooks:
- type: command
command: "echo '[planning-with-files] File updated. If this completes a phase, update task_plan.md status.'"
Stop:
- hooks:
- type: command
command: |
if command -v pwsh &> /dev/null && [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" || "$OS" == "Windows_NT" ]]; then
pwsh -ExecutionPolicy Bypass -File "${CLAUDE_PLUGIN_ROOT}/scripts/check-complete.ps1" 2>/dev/null || powershell -ExecutionPolicy Bypass -File "${CLAUDE_PLUGIN_ROOT}/scripts/check-complete.ps1" 2>/dev/null || bash "${CLAUDE_PLUGIN_ROOT}/scripts/check-complete.sh"
else
bash "${CLAUDE_PLUGIN_ROOT}/scripts/check-complete.sh"
fi
license: MIT
metadata:
version: "2.4.0"
author: OthmanAdi
allowed-tools: Read Write Edit Bash Glob Grep WebFetch WebSearch
---
# Planning with Files
Work like Manus: Use persistent markdown files as your "working memory on disk."
## FIRST: Check for Previous Session (v2.2.0)
## FIRST: Check for Previous Session
**Before starting work**, check for unsynced context from a previous session:
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/session-catchup.py "$(pwd)"
python3 scripts/session-catchup.py "$(pwd)"
```
If catchup report shows unsynced context:
@@ -54,12 +28,12 @@ If catchup report shows unsynced context:
## Important: Where Files Go
- **Templates** are in `${CLAUDE_PLUGIN_ROOT}/templates/`
- **Templates** are in `templates/`
- **Your planning files** go in **your project directory**
| Location | What Goes There |
|----------|-----------------|
| Skill directory (`${CLAUDE_PLUGIN_ROOT}/`) | Templates, scripts, reference docs |
| Skill directory | Templates, scripts, reference docs |
| Your project directory | `task_plan.md`, `findings.md`, `progress.md` |
## Quick Start
@@ -203,7 +177,7 @@ Helper scripts for automation:
- `scripts/init-session.sh` — Initialize all planning files
- `scripts/check-complete.sh` — Verify all phases complete
- `scripts/session-catchup.py` — Recover context from previous session (v2.2.0)
- `scripts/session-catchup.py` — Recover context from previous session
## Advanced Topics
+180 -110
View File
@@ -2,8 +2,9 @@
"""
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.
Session-agnostic scanning: finds the most recent planning file update across
ALL sessions, then collects all conversation from that point forward through
all subsequent sessions until now.
Usage: python3 session-catchup.py [project-path]
"""
@@ -13,7 +14,6 @@ import sys
import os
from pathlib import Path
from typing import List, Dict, Optional, Tuple
from datetime import datetime
PLANNING_FILES = ['task_plan.md', 'progress.md', 'findings.md']
@@ -34,106 +34,147 @@ def get_sessions_sorted(project_dir: Path) -> List[Path]:
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') 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 get_session_first_timestamp(session_file: Path) -> Optional[str]:
"""Get the timestamp of the first message in a session."""
try:
with open(session_file, 'r') as f:
for line in f:
try:
data = json.loads(line)
ts = data.get('timestamp')
if ts:
return ts
except:
continue
except:
pass
return None
def find_last_planning_update(messages: List[Dict]) -> Tuple[int, Optional[str]]:
def scan_for_planning_update(session_file: Path) -> Tuple[int, Optional[str]]:
"""
Find the last time a planning file was written/edited.
Returns (line_number, filename) or (-1, None) if not found.
Quickly scan a session file for planning file updates.
Returns (line_number, filename) of last update, or (-1, None) if none found.
"""
last_update_line = -1
last_update_file = None
for msg in messages:
msg_type = msg.get('type')
try:
with open(session_file, 'r') as f:
for line_num, line in enumerate(f):
if '"Write"' not in line and '"Edit"' not in line:
continue
if msg_type == 'assistant':
content = msg.get('message', {}).get('content', [])
if isinstance(content, list):
for item in content:
if item.get('type') == 'tool_use':
try:
data = json.loads(line)
if data.get('type') != 'assistant':
continue
content = data.get('message', {}).get('content', [])
if not isinstance(content, list):
continue
for item in content:
if item.get('type') != 'tool_use':
continue
tool_name = item.get('name', '')
tool_input = item.get('input', {})
if tool_name not in ('Write', 'Edit'):
continue
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
file_path = item.get('input', {}).get('file_path', '')
for pf in PLANNING_FILES:
if file_path.endswith(pf):
last_update_line = line_num
last_update_file = pf
break
except json.JSONDecodeError:
continue
except Exception:
pass
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."""
def extract_messages_from_session(session_file: Path, after_line: int = -1) -> List[Dict]:
"""
Extract conversation messages from a session file.
If after_line >= 0, only extract messages after that line.
If after_line < 0, extract all messages.
"""
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')):
try:
with open(session_file, 'r') as f:
for line_num, line in enumerate(f):
if after_line >= 0 and line_num <= after_line:
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 = []
try:
msg = json.loads(line)
except json.JSONDecodeError:
continue
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}")
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:
tool_uses.append(f"{tool_name}")
content = ''
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']
})
if content and isinstance(content, str):
# Skip system/command messages
if content.startswith(('<local-command', '<command-', '<task-notification')):
continue
if len(content) > 20:
result.append({
'role': 'user',
'content': content,
'line': line_num,
'session': session_file.stem[:8]
})
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}")
elif tool_name == 'AskUserQuestion':
tool_uses.append("AskUserQuestion")
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': line_num,
'session': session_file.stem[:8]
})
except Exception:
pass
return result
@@ -142,53 +183,82 @@ def main():
project_path = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
project_dir = get_project_dir(project_path)
# Check if planning files exist (indicates active task)
has_planning_files = any(
Path(project_path, f).exists() for f in PLANNING_FILES
)
if not project_dir.exists():
# No previous sessions, nothing to catch up on
return
sessions = get_sessions_sorted(project_dir)
if len(sessions) < 1:
if len(sessions) < 2:
return
# Find a substantial previous session
target_session = None
for session in sessions:
if session.stat().st_size > 5000:
target_session = session
# Skip the current session (most recently modified = index 0)
previous_sessions = sessions[1:]
# Find the most recent planning file update across ALL previous sessions
# Sessions are sorted newest first, so we scan in order
update_session = None
update_line = -1
update_file = None
update_session_idx = -1
for idx, session in enumerate(previous_sessions):
line, filename = scan_for_planning_update(session)
if line >= 0:
update_session = session
update_line = line
update_file = filename
update_session_idx = idx
break
if not target_session:
if not update_session:
# No planning file updates found in any previous session
return
messages = parse_session_messages(target_session)
last_update_line, last_update_file = find_last_planning_update(messages)
# Collect ALL messages from the update point forward, across all sessions
all_messages = []
# Only output if there's unsynced content
if last_update_line < 0:
messages_after = extract_messages_after(messages, len(messages) - 30)
else:
messages_after = extract_messages_after(messages, last_update_line)
# 1. Get messages from the session with the update (after the update line)
messages_from_update_session = extract_messages_from_session(update_session, after_line=update_line)
all_messages.extend(messages_from_update_session)
if not messages_after:
# 2. Get ALL messages from sessions between update_session and current
# These are sessions[1:update_session_idx] (newer than update_session)
intermediate_sessions = previous_sessions[:update_session_idx]
# Process from oldest to newest for correct chronological order
for session in reversed(intermediate_sessions):
messages = extract_messages_from_session(session, after_line=-1) # Get all messages
all_messages.extend(messages)
if not all_messages:
return
# Output catchup report
print("\n[planning-with-files] SESSION CATCHUP DETECTED")
print(f"Previous session: {target_session.stem}")
print(f"Last planning update: {update_file} in session {update_session.stem[:8]}...")
if last_update_line >= 0:
print(f"Last planning update: {last_update_file} at message #{last_update_line}")
print(f"Unsynced messages: {len(messages_after)}")
else:
print("No planning file updates found in previous session")
sessions_covered = update_session_idx + 1
if sessions_covered > 1:
print(f"Scanning {sessions_covered} sessions for unsynced context")
print(f"Unsynced messages: {len(all_messages)}")
print("\n--- UNSYNCED CONTEXT ---")
for msg in messages_after[-15:]: # Last 15 messages
# Show up to 100 messages
MAX_MESSAGES = 100
if len(all_messages) > MAX_MESSAGES:
print(f"(Showing last {MAX_MESSAGES} of {len(all_messages)} messages)\n")
messages_to_show = all_messages[-MAX_MESSAGES:]
else:
messages_to_show = all_messages
current_session = None
for msg in messages_to_show:
# Show session marker when it changes
if msg.get('session') != current_session:
current_session = msg.get('session')
print(f"\n[Session: {current_session}...]")
if msg['role'] == 'user':
print(f"USER: {msg['content'][:300]}")
else:
+10 -36
View File
@@ -1,49 +1,23 @@
---
name: planning-with-files
version: "2.3.0"
description: Implements Manus-style file-based planning for complex tasks. Creates task_plan.md, findings.md, and progress.md. Use when starting complex multi-step tasks, research projects, or any task requiring >5 tool calls. Now with automatic session recovery after /clear.
user-invocable: true
allowed-tools:
- Read
- Write
- Edit
- Bash
- Glob
- Grep
- WebFetch
- WebSearch
hooks:
PreToolUse:
- matcher: "Write|Edit|Bash|Read|Glob|Grep"
hooks:
- type: command
command: "cat task_plan.md 2>/dev/null | head -30 || true"
PostToolUse:
- matcher: "Write|Edit"
hooks:
- type: command
command: "echo '[planning-with-files] File updated. If this completes a phase, update task_plan.md status.'"
Stop:
- hooks:
- type: command
command: |
if command -v pwsh &> /dev/null && [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" || "$OS" == "Windows_NT" ]]; then
pwsh -ExecutionPolicy Bypass -File "${CLAUDE_PLUGIN_ROOT}/scripts/check-complete.ps1" 2>/dev/null || powershell -ExecutionPolicy Bypass -File "${CLAUDE_PLUGIN_ROOT}/scripts/check-complete.ps1" 2>/dev/null || bash "${CLAUDE_PLUGIN_ROOT}/scripts/check-complete.sh"
else
bash "${CLAUDE_PLUGIN_ROOT}/scripts/check-complete.sh"
fi
license: MIT
metadata:
version: "2.4.0"
author: OthmanAdi
allowed-tools: Read Write Edit Bash Glob Grep WebFetch WebSearch
---
# Planning with Files
Work like Manus: Use persistent markdown files as your "working memory on disk."
## FIRST: Check for Previous Session (v2.2.0)
## FIRST: Check for Previous Session
**Before starting work**, check for unsynced context from a previous session:
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/session-catchup.py "$(pwd)"
python3 scripts/session-catchup.py "$(pwd)"
```
If catchup report shows unsynced context:
@@ -54,12 +28,12 @@ If catchup report shows unsynced context:
## Important: Where Files Go
- **Templates** are in `${CLAUDE_PLUGIN_ROOT}/templates/`
- **Templates** are in `templates/`
- **Your planning files** go in **your project directory**
| Location | What Goes There |
|----------|-----------------|
| Skill directory (`${CLAUDE_PLUGIN_ROOT}/`) | Templates, scripts, reference docs |
| Skill directory | Templates, scripts, reference docs |
| Your project directory | `task_plan.md`, `findings.md`, `progress.md` |
## Quick Start
@@ -203,7 +177,7 @@ Helper scripts for automation:
- `scripts/init-session.sh` — Initialize all planning files
- `scripts/check-complete.sh` — Verify all phases complete
- `scripts/session-catchup.py` — Recover context from previous session (v2.2.0)
- `scripts/session-catchup.py` — Recover context from previous session
## Advanced Topics