aiwcli 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -35
- package/dist/lib/template-installer.js +38 -0
- package/dist/templates/_shared/.claude/commands/handoff.md +219 -7
- package/dist/templates/_shared/.codex/workflows/handoff.md +219 -7
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +219 -7
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/context_enforcer.py +73 -19
- package/dist/templates/_shared/hooks/context_monitor.py +28 -10
- package/dist/templates/_shared/hooks/file-suggestion.py +45 -15
- package/dist/templates/_shared/hooks/session_start.py +108 -0
- package/dist/templates/_shared/hooks/task_create_atomicity.py +199 -0
- package/dist/templates/_shared/hooks/task_create_capture.py +2 -2
- package/dist/templates/_shared/hooks/task_update_capture.py +2 -2
- package/dist/templates/_shared/hooks/user_prompt_submit.py +58 -23
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/constants.py +45 -0
- package/dist/templates/_shared/lib/base/inference.py +29 -21
- package/dist/templates/_shared/lib/base/stop_words.py +158 -0
- package/dist/templates/_shared/lib/base/subprocess_utils.py +46 -0
- package/dist/templates/_shared/lib/base/utils.py +6 -3
- package/dist/templates/_shared/lib/context/__init__.py +0 -10
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/cache.py +2 -4
- package/dist/templates/_shared/lib/context/context_manager.py +2 -119
- package/dist/templates/_shared/lib/context/discovery.py +8 -50
- package/dist/templates/_shared/lib/context/task_sync.py +5 -82
- package/dist/templates/_shared/lib/handoff/document_generator.py +2 -5
- package/dist/templates/_shared/lib/templates/README.md +0 -1
- package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/formatters.py +0 -1
- package/dist/templates/_shared/lib/templates/persona_questions.py +113 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +13 -27
- package/dist/templates/_shared/scripts/save_handoff.py +289 -43
- package/dist/templates/_shared/workflows/handoff.md +30 -16
- package/dist/templates/cc-native/.claude/agents/cc-native/ACCESSIBILITY-TESTER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/ARCHITECT-REVIEWER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/ASSUMPTION-CHAIN-TRACER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/CLARITY-AUDITOR.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/CODE-REVIEWER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/COMPLETENESS-CHECKER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/CONTEXT-EXTRACTOR.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/DEVILS-ADVOCATE.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/FEASIBILITY-ANALYST.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/FRESH-PERSPECTIVE.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/HANDOFF-READINESS.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/HIDDEN-COMPLEXITY-DETECTOR.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/INCENTIVE-MAPPER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/PENETRATION-TESTER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/PERFORMANCE-ENGINEER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/PLAN-ORCHESTRATOR.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/PRECEDENT-FINDER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/REVERSIBILITY-ANALYST.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/RISK-ASSESSOR.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/SECOND-ORDER-ANALYST.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/SIMPLICITY-GUARDIAN.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/SKEPTIC.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/STAKEHOLDER-ADVOCATE.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/TRADE-OFF-ILLUMINATOR.md +1 -1
- package/dist/templates/cc-native/.claude/settings.json +21 -0
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +211 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +87 -27
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +240 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +10 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +10 -0
- package/dist/templates/cc-native/_cc-native/lib/utils.py +123 -10
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +6 -0
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/templates/cc-native/_cc-native/docs/PERMISSION_REQUEST_VERIFICATION.md +0 -147
|
Binary file
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Persona-based question templates for plan clarification.
|
|
2
|
+
|
|
3
|
+
Uses distinct reasoning lenses to surface hidden constraints and assumptions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import List, Dict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class PersonaQuestion:
|
|
12
|
+
"""A clarifying question from a specific persona lens."""
|
|
13
|
+
|
|
14
|
+
persona: str
|
|
15
|
+
display_name: str
|
|
16
|
+
question: str
|
|
17
|
+
purpose: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
CLARIFICATION_PERSONAS: Dict[str, List[PersonaQuestion]] = {
|
|
21
|
+
"problem_validator": [
|
|
22
|
+
PersonaQuestion(
|
|
23
|
+
persona="problem_validator",
|
|
24
|
+
display_name="Questioning the Problem",
|
|
25
|
+
question="Can you describe the problem you're trying to solve without mentioning the solution?",
|
|
26
|
+
purpose="Separates problem from solution to check alignment",
|
|
27
|
+
),
|
|
28
|
+
PersonaQuestion(
|
|
29
|
+
persona="problem_validator",
|
|
30
|
+
display_name="Challenging the Approach",
|
|
31
|
+
question="What's the simplest possible way to achieve this outcome that we haven't considered?",
|
|
32
|
+
purpose="Identifies potential over-engineering",
|
|
33
|
+
),
|
|
34
|
+
],
|
|
35
|
+
"assumption_validator": [
|
|
36
|
+
PersonaQuestion(
|
|
37
|
+
persona="assumption_validator",
|
|
38
|
+
display_name="Surfacing Assumptions",
|
|
39
|
+
question="What must already be true about your users, systems, or constraints for this to succeed?",
|
|
40
|
+
purpose="Surfaces foundational assumptions that could invalidate the plan",
|
|
41
|
+
),
|
|
42
|
+
PersonaQuestion(
|
|
43
|
+
persona="assumption_validator",
|
|
44
|
+
display_name="Hidden Dependencies",
|
|
45
|
+
question="What are you assuming 'everyone knows' about this problem that might not be documented?",
|
|
46
|
+
purpose="Uncovers implicit knowledge that needs to be made explicit",
|
|
47
|
+
),
|
|
48
|
+
],
|
|
49
|
+
"user_advocate": [
|
|
50
|
+
PersonaQuestion(
|
|
51
|
+
persona="user_advocate",
|
|
52
|
+
display_name="Understanding Users",
|
|
53
|
+
question="Who specifically will use this, and what problem does it solve for them today?",
|
|
54
|
+
purpose="Grounds the plan in actual user needs",
|
|
55
|
+
),
|
|
56
|
+
PersonaQuestion(
|
|
57
|
+
persona="user_advocate",
|
|
58
|
+
display_name="Impact Assessment",
|
|
59
|
+
question="If we did nothing, what would happen? Who would be affected?",
|
|
60
|
+
purpose="Establishes urgency and stakes",
|
|
61
|
+
),
|
|
62
|
+
],
|
|
63
|
+
"tradeoff_illuminator": [
|
|
64
|
+
PersonaQuestion(
|
|
65
|
+
persona="tradeoff_illuminator",
|
|
66
|
+
display_name="Revealing Trade-offs",
|
|
67
|
+
question="What are you willing to sacrifice (scope, time, quality, features) to make this work?",
|
|
68
|
+
purpose="Forces explicit prioritization",
|
|
69
|
+
),
|
|
70
|
+
PersonaQuestion(
|
|
71
|
+
persona="tradeoff_illuminator",
|
|
72
|
+
display_name="Foreclosed Options",
|
|
73
|
+
question="What becomes harder or impossible to do later if we proceed this way?",
|
|
74
|
+
purpose="Surfaces opportunity costs and lock-in risks",
|
|
75
|
+
),
|
|
76
|
+
],
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_all_persona_questions() -> List[PersonaQuestion]:
|
|
81
|
+
"""Get all persona questions as a flat list."""
|
|
82
|
+
questions = []
|
|
83
|
+
for persona_qs in CLARIFICATION_PERSONAS.values():
|
|
84
|
+
questions.extend(persona_qs)
|
|
85
|
+
return questions
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def format_questions_for_prompt() -> str:
|
|
89
|
+
"""Format persona questions for injection into Claude prompt."""
|
|
90
|
+
lines = [
|
|
91
|
+
"### Persona-Based Clarifying Questions",
|
|
92
|
+
"",
|
|
93
|
+
"Ask 5-8 questions from these perspectives using AskUserQuestion:",
|
|
94
|
+
"",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
for persona_qs in CLARIFICATION_PERSONAS.values():
|
|
98
|
+
for q in persona_qs:
|
|
99
|
+
lines.append(f"**{q.display_name}**")
|
|
100
|
+
lines.append(f'- Q: "{q.question}"')
|
|
101
|
+
lines.append(f"- Purpose: {q.purpose}")
|
|
102
|
+
lines.append("")
|
|
103
|
+
|
|
104
|
+
lines.extend(
|
|
105
|
+
[
|
|
106
|
+
"**Guidance:**",
|
|
107
|
+
"- Select questions most relevant to THIS plan (skip if already answered)",
|
|
108
|
+
"- Ask one at a time with clear context",
|
|
109
|
+
"- Use answers to refine the plan before ExitPlanMode",
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return "\n".join(lines)
|
|
@@ -72,47 +72,33 @@ This context allows reviewers to assess whether your plan addresses the user's n
|
|
|
72
72
|
def get_questions_offer_template() -> str:
|
|
73
73
|
"""Get the clarifying questions offer template.
|
|
74
74
|
|
|
75
|
+
Uses persona-based questioning to surface hidden constraints.
|
|
76
|
+
|
|
75
77
|
Returns:
|
|
76
78
|
Formatted markdown prompt for offering clarifying questions
|
|
77
79
|
"""
|
|
78
|
-
|
|
80
|
+
from .persona_questions import format_questions_for_prompt
|
|
81
|
+
|
|
82
|
+
persona_questions = format_questions_for_prompt()
|
|
83
|
+
|
|
84
|
+
return f"""
|
|
79
85
|
## First Plan Write - Optional Clarifying Questions
|
|
80
86
|
|
|
81
87
|
Your initial plan has been saved. Before finalizing, ask the user if they'd like to answer clarifying questions to refine it.
|
|
82
88
|
|
|
83
|
-
**Use AskUserQuestion now
|
|
89
|
+
**Use AskUserQuestion now:**
|
|
84
90
|
|
|
85
91
|
Header: "Questions?"
|
|
86
|
-
Question: "I've drafted an initial plan. Would you like to answer a few clarifying questions so I can refine it?"
|
|
92
|
+
Question: "I've drafted an initial plan. Would you like to answer a few clarifying questions from different perspectives so I can refine it?"
|
|
87
93
|
Options:
|
|
88
|
-
- "Yes, ask me questions" (description: "I'll
|
|
94
|
+
- "Yes, ask me questions" (description: "I'll ask targeted questions to surface hidden constraints, then update the plan")
|
|
89
95
|
- "No, proceed as-is" (description: "Skip questions and proceed with the current plan")
|
|
90
96
|
|
|
91
|
-
### If user chooses YES
|
|
92
|
-
|
|
93
|
-
1. **Technical Implementation**
|
|
94
|
-
- Preferred approaches or patterns?
|
|
95
|
-
- Technologies/libraries to use or avoid?
|
|
96
|
-
- Performance or scalability requirements?
|
|
97
|
-
|
|
98
|
-
2. **Constraints & Requirements**
|
|
99
|
-
- Hard constraints that must be respected?
|
|
100
|
-
- Deadlines or scope limitations?
|
|
101
|
-
- Dependencies on other systems?
|
|
102
|
-
|
|
103
|
-
3. **Edge Cases & Concerns**
|
|
104
|
-
- Known edge cases to handle?
|
|
105
|
-
- Security or privacy considerations?
|
|
106
|
-
- Error handling preferences?
|
|
107
|
-
|
|
108
|
-
4. **Tradeoffs**
|
|
109
|
-
- Speed vs. quality preferences?
|
|
110
|
-
- Simplicity vs. flexibility?
|
|
111
|
-
- What's acceptable to defer to later?
|
|
97
|
+
### If user chooses YES:
|
|
112
98
|
|
|
113
|
-
|
|
99
|
+
{persona_questions}
|
|
114
100
|
|
|
115
|
-
After gathering answers, **update the plan file** with
|
|
101
|
+
After gathering answers, **update the plan file** with refined content before calling ExitPlanMode.
|
|
116
102
|
|
|
117
103
|
### If user chooses NO:
|
|
118
104
|
Proceed directly to ExitPlanMode with the current plan.
|
|
@@ -1,40 +1,221 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Save a handoff document
|
|
2
|
+
"""Save a handoff document with folder-based sharding.
|
|
3
3
|
|
|
4
4
|
Usage:
|
|
5
5
|
python .aiwcli/_shared/scripts/save_handoff.py <context_id> <<'EOF'
|
|
6
|
-
# Your handoff markdown content here
|
|
6
|
+
# Your handoff markdown content here (with <!-- SECTION: name --> markers)
|
|
7
7
|
EOF
|
|
8
8
|
|
|
9
9
|
Or with a file:
|
|
10
10
|
python .aiwcli/_shared/scripts/save_handoff.py <context_id> < handoff.md
|
|
11
11
|
|
|
12
12
|
This script:
|
|
13
|
-
1.
|
|
14
|
-
2.
|
|
15
|
-
3.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
1. Parses sections from incoming markdown using <!-- SECTION: name --> markers
|
|
14
|
+
2. Creates a timestamped folder at _output/contexts/{context_id}/handoffs/{YYYY-MM-DD-HHMM}/
|
|
15
|
+
3. Writes sharded files:
|
|
16
|
+
- index.md (main entry point with navigation)
|
|
17
|
+
- completed-work.md, dead-ends.md, decisions.md, pending.md, context.md
|
|
18
|
+
- plan.md (copy of original plan if it exists)
|
|
19
|
+
4. Records the event in events.jsonl (informational only)
|
|
19
20
|
"""
|
|
21
|
+
import json
|
|
22
|
+
import re
|
|
23
|
+
import subprocess
|
|
20
24
|
import sys
|
|
21
25
|
from datetime import datetime
|
|
22
26
|
from pathlib import Path
|
|
27
|
+
from typing import Dict, Optional
|
|
23
28
|
|
|
24
29
|
# Add parent directories to path for imports
|
|
25
30
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
26
31
|
SHARED_ROOT = SCRIPT_DIR.parent
|
|
27
32
|
sys.path.insert(0, str(SHARED_ROOT))
|
|
28
33
|
|
|
29
|
-
from lib.context.context_manager import
|
|
30
|
-
from lib.base.utils import eprint
|
|
34
|
+
from lib.context.context_manager import get_context
|
|
35
|
+
from lib.base.utils import eprint
|
|
36
|
+
from lib.base.atomic_write import atomic_write
|
|
37
|
+
from lib.base.constants import get_handoff_folder_path
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_frontmatter(content: str) -> tuple[Dict[str, str], str]:
|
|
41
|
+
"""Parse YAML frontmatter from markdown content.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Tuple of (frontmatter dict, remaining content)
|
|
45
|
+
"""
|
|
46
|
+
frontmatter = {}
|
|
47
|
+
remaining = content
|
|
48
|
+
|
|
49
|
+
if content.startswith('---'):
|
|
50
|
+
parts = content.split('---', 2)
|
|
51
|
+
if len(parts) >= 3:
|
|
52
|
+
fm_lines = parts[1].strip().split('\n')
|
|
53
|
+
for line in fm_lines:
|
|
54
|
+
if ':' in line:
|
|
55
|
+
key, value = line.split(':', 1)
|
|
56
|
+
frontmatter[key.strip()] = value.strip()
|
|
57
|
+
remaining = parts[2].strip()
|
|
58
|
+
|
|
59
|
+
return frontmatter, remaining
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def parse_handoff_sections(content: str) -> Dict[str, str]:
|
|
63
|
+
"""Parse markdown content by section markers.
|
|
64
|
+
|
|
65
|
+
Looks for <!-- SECTION: name --> markers and extracts content between them.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dict mapping section names to their content
|
|
69
|
+
"""
|
|
70
|
+
sections = {}
|
|
71
|
+
current_section = None
|
|
72
|
+
current_content = []
|
|
73
|
+
|
|
74
|
+
for line in content.split('\n'):
|
|
75
|
+
if line.strip().startswith('<!-- SECTION:'):
|
|
76
|
+
# Save previous section
|
|
77
|
+
if current_section:
|
|
78
|
+
sections[current_section] = '\n'.join(current_content).strip()
|
|
79
|
+
# Extract new section name
|
|
80
|
+
match = re.search(r'<!-- SECTION:\s*(\S+)\s*-->', line)
|
|
81
|
+
if match:
|
|
82
|
+
current_section = match.group(1)
|
|
83
|
+
current_content = []
|
|
84
|
+
elif current_section:
|
|
85
|
+
current_content.append(line)
|
|
86
|
+
|
|
87
|
+
# Save final section
|
|
88
|
+
if current_section:
|
|
89
|
+
sections[current_section] = '\n'.join(current_content).strip()
|
|
90
|
+
|
|
91
|
+
return sections
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_git_status() -> str:
|
|
95
|
+
"""Get current git status."""
|
|
96
|
+
try:
|
|
97
|
+
result = subprocess.run(
|
|
98
|
+
['git', 'status', '--short'],
|
|
99
|
+
capture_output=True,
|
|
100
|
+
text=True,
|
|
101
|
+
timeout=5
|
|
102
|
+
)
|
|
103
|
+
return result.stdout.strip() or "(no changes)"
|
|
104
|
+
except Exception:
|
|
105
|
+
return "(git status unavailable)"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_plan_path_from_context(context_id: str, project_root: Path) -> Optional[Path]:
|
|
109
|
+
"""Get the plan path from context.json if available."""
|
|
110
|
+
context = get_context(context_id, project_root)
|
|
111
|
+
if not context or not context.in_flight:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
artifact_path = context.in_flight.artifact_path
|
|
115
|
+
if artifact_path:
|
|
116
|
+
plan_path = Path(artifact_path)
|
|
117
|
+
if plan_path.exists():
|
|
118
|
+
return plan_path
|
|
119
|
+
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def generate_index(
|
|
124
|
+
frontmatter: Dict[str, str],
|
|
125
|
+
sections: Dict[str, str],
|
|
126
|
+
git_status: str,
|
|
127
|
+
has_plan: bool,
|
|
128
|
+
) -> str:
|
|
129
|
+
"""Generate the index.md file with summary and navigation."""
|
|
130
|
+
now = datetime.now()
|
|
131
|
+
|
|
132
|
+
lines = [
|
|
133
|
+
"---",
|
|
134
|
+
"type: handoff",
|
|
135
|
+
f"context_id: {frontmatter.get('context_id', 'unknown')}",
|
|
136
|
+
f"created_at: {now.isoformat()}",
|
|
137
|
+
f"session_id: {frontmatter.get('session_id', 'unknown')}",
|
|
138
|
+
f"project: {frontmatter.get('project', 'unknown')}",
|
|
139
|
+
f"plan_path: {frontmatter.get('plan_document', 'none')}",
|
|
140
|
+
"---",
|
|
141
|
+
"",
|
|
142
|
+
f"# Session Handoff - {now.strftime('%Y-%m-%d %H:%M')}",
|
|
143
|
+
"",
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
# Summary section
|
|
147
|
+
summary = sections.get('summary', '').strip()
|
|
148
|
+
if summary:
|
|
149
|
+
# Extract just the content (skip the ## Summary header if present)
|
|
150
|
+
summary_lines = summary.split('\n')
|
|
151
|
+
summary_text = '\n'.join(
|
|
152
|
+
line for line in summary_lines
|
|
153
|
+
if not line.strip().startswith('##')
|
|
154
|
+
).strip()
|
|
155
|
+
lines.extend([
|
|
156
|
+
"## Summary",
|
|
157
|
+
summary_text,
|
|
158
|
+
"",
|
|
159
|
+
])
|
|
160
|
+
|
|
161
|
+
# Navigation table
|
|
162
|
+
lines.extend([
|
|
163
|
+
"## Quick Navigation",
|
|
164
|
+
"",
|
|
165
|
+
"| Document | Purpose | Priority |",
|
|
166
|
+
"|----------|---------|----------|",
|
|
167
|
+
"| [Dead Ends](./dead-ends.md) | Failed approaches - DO NOT RETRY | Read First |",
|
|
168
|
+
"| [Pending](./pending.md) | Next steps and blockers | Action Items |",
|
|
169
|
+
"| [Completed Work](./completed-work.md) | Tasks finished this session | Reference |",
|
|
170
|
+
"| [Decisions](./decisions.md) | Technical choices and rationale | Reference |",
|
|
171
|
+
])
|
|
172
|
+
|
|
173
|
+
if has_plan:
|
|
174
|
+
lines.append("| [Plan](./plan.md) | Original plan being implemented | Reference |")
|
|
175
|
+
|
|
176
|
+
lines.extend([
|
|
177
|
+
"| [Context](./context.md) | External requirements and notes | Reference |",
|
|
178
|
+
"",
|
|
179
|
+
"## Continuation Instructions",
|
|
180
|
+
"",
|
|
181
|
+
"To continue this work in a new session:",
|
|
182
|
+
"1. This index document provides the overview",
|
|
183
|
+
"2. **Read [Dead Ends](./dead-ends.md) first** to avoid repeating failed approaches",
|
|
184
|
+
"3. Check [Pending](./pending.md) for immediate next steps",
|
|
185
|
+
"4. Reference other documents as needed",
|
|
186
|
+
"",
|
|
187
|
+
"## Git Status at Handoff",
|
|
188
|
+
"```",
|
|
189
|
+
git_status,
|
|
190
|
+
"```",
|
|
191
|
+
"",
|
|
192
|
+
])
|
|
193
|
+
|
|
194
|
+
return '\n'.join(lines)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def write_section_file(folder: Path, filename: str, title: str, content: str) -> bool:
|
|
198
|
+
"""Write a section file with header."""
|
|
199
|
+
lines = [
|
|
200
|
+
f"# {title}",
|
|
201
|
+
"",
|
|
202
|
+
content if content else "(No content for this section)",
|
|
203
|
+
"",
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
file_path = folder / filename
|
|
207
|
+
success, error = atomic_write(file_path, '\n'.join(lines))
|
|
208
|
+
if not success:
|
|
209
|
+
eprint(f"[save_handoff] Warning: Failed to write {filename}: {error}")
|
|
210
|
+
return False
|
|
211
|
+
return True
|
|
31
212
|
|
|
32
213
|
|
|
33
214
|
def main():
|
|
34
215
|
if len(sys.argv) < 2:
|
|
35
216
|
print("Usage: python save_handoff.py <context_id> < content.md", file=sys.stderr)
|
|
36
217
|
print(" python save_handoff.py <context_id> <<'EOF'", file=sys.stderr)
|
|
37
|
-
print(" ... markdown content ...", file=sys.stderr)
|
|
218
|
+
print(" ... markdown content with <!-- SECTION: name --> markers ...", file=sys.stderr)
|
|
38
219
|
print(" EOF", file=sys.stderr)
|
|
39
220
|
sys.exit(1)
|
|
40
221
|
|
|
@@ -55,44 +236,109 @@ def main():
|
|
|
55
236
|
print(f"Error: Context not found: {context_id}", file=sys.stderr)
|
|
56
237
|
sys.exit(1)
|
|
57
238
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
239
|
+
# Parse frontmatter and sections
|
|
240
|
+
frontmatter, body = parse_frontmatter(content)
|
|
241
|
+
sections = parse_handoff_sections(body)
|
|
61
242
|
|
|
62
|
-
|
|
63
|
-
timestamp = datetime.now().strftime("%H%M")
|
|
64
|
-
filename = f"HANDOFF-{timestamp}.md"
|
|
65
|
-
file_path = handoffs_dir / filename
|
|
243
|
+
eprint(f"[save_handoff] Parsed {len(sections)} sections: {list(sections.keys())}")
|
|
66
244
|
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
file_path = handoffs_dir / filename
|
|
72
|
-
counter += 1
|
|
245
|
+
# Create handoff folder
|
|
246
|
+
handoff_folder = get_handoff_folder_path(context_id, project_root)
|
|
247
|
+
handoff_folder.mkdir(parents=True, exist_ok=True)
|
|
248
|
+
eprint(f"[save_handoff] Created folder: {handoff_folder}")
|
|
73
249
|
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
250
|
+
# Get git status
|
|
251
|
+
git_status = get_git_status()
|
|
252
|
+
|
|
253
|
+
# Check for plan
|
|
254
|
+
plan_path = get_plan_path_from_context(context_id, project_root)
|
|
255
|
+
has_plan = plan_path is not None
|
|
256
|
+
|
|
257
|
+
# Copy plan if exists
|
|
258
|
+
if plan_path:
|
|
259
|
+
try:
|
|
260
|
+
plan_content = plan_path.read_text(encoding='utf-8')
|
|
261
|
+
plan_dest = handoff_folder / "plan.md"
|
|
262
|
+
success, error = atomic_write(plan_dest, plan_content)
|
|
263
|
+
if success:
|
|
264
|
+
eprint(f"[save_handoff] Copied plan from {plan_path}")
|
|
265
|
+
else:
|
|
266
|
+
eprint(f"[save_handoff] Warning: Failed to copy plan: {error}")
|
|
267
|
+
except Exception as e:
|
|
268
|
+
eprint(f"[save_handoff] Warning: Failed to read plan: {e}")
|
|
269
|
+
|
|
270
|
+
# Write index.md
|
|
271
|
+
index_content = generate_index(frontmatter, sections, git_status, has_plan)
|
|
272
|
+
index_path = handoff_folder / "index.md"
|
|
273
|
+
success, error = atomic_write(index_path, index_content)
|
|
274
|
+
if not success:
|
|
275
|
+
print(f"Error: Failed to write index.md: {error}", file=sys.stderr)
|
|
82
276
|
sys.exit(1)
|
|
83
277
|
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
278
|
+
# Write section files
|
|
279
|
+
section_mapping = {
|
|
280
|
+
'completed': ('completed-work.md', 'Work Completed'),
|
|
281
|
+
'dead-ends': ('dead-ends.md', 'Dead Ends - Do Not Retry'),
|
|
282
|
+
'decisions': ('decisions.md', 'Key Decisions'),
|
|
283
|
+
'pending': ('pending.md', 'Pending Issues'),
|
|
284
|
+
'next-steps': ('pending.md', None), # Append to pending.md
|
|
285
|
+
'files': ('completed-work.md', None), # Append to completed-work.md
|
|
286
|
+
'context': ('context.md', 'Context for Future Sessions'),
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
# Track which files we've written with their content
|
|
290
|
+
file_contents: Dict[str, list] = {}
|
|
291
|
+
|
|
292
|
+
for section_name, (filename, title) in section_mapping.items():
|
|
293
|
+
section_content = sections.get(section_name, '')
|
|
294
|
+
if not section_content:
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
if title is None:
|
|
298
|
+
# Append mode - add to existing content
|
|
299
|
+
if filename not in file_contents:
|
|
300
|
+
file_contents[filename] = []
|
|
301
|
+
file_contents[filename].append(section_content)
|
|
302
|
+
else:
|
|
303
|
+
# Write mode - set as main content with title
|
|
304
|
+
if filename not in file_contents:
|
|
305
|
+
file_contents[filename] = [f"# {title}", "", section_content]
|
|
306
|
+
else:
|
|
307
|
+
# Insert title at beginning if not present
|
|
308
|
+
file_contents[filename] = [f"# {title}", ""] + file_contents[filename] + ["", section_content]
|
|
309
|
+
|
|
310
|
+
# Write all accumulated content
|
|
311
|
+
for filename, content_parts in file_contents.items():
|
|
312
|
+
file_path = handoff_folder / filename
|
|
313
|
+
full_content = '\n'.join(content_parts) + '\n'
|
|
314
|
+
success, error = atomic_write(file_path, full_content)
|
|
315
|
+
if not success:
|
|
316
|
+
eprint(f"[save_handoff] Warning: Failed to write {filename}: {error}")
|
|
317
|
+
|
|
318
|
+
# Ensure all expected files exist (even if empty)
|
|
319
|
+
expected_files = ['completed-work.md', 'dead-ends.md', 'decisions.md', 'pending.md', 'context.md']
|
|
320
|
+
titles = {
|
|
321
|
+
'completed-work.md': 'Work Completed',
|
|
322
|
+
'dead-ends.md': 'Dead Ends - Do Not Retry',
|
|
323
|
+
'decisions.md': 'Key Decisions',
|
|
324
|
+
'pending.md': 'Pending Issues & Next Steps',
|
|
325
|
+
'context.md': 'Context for Future Sessions',
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
for filename in expected_files:
|
|
329
|
+
file_path = handoff_folder / filename
|
|
330
|
+
if not file_path.exists():
|
|
331
|
+
write_section_file(handoff_folder, filename, titles[filename], "")
|
|
332
|
+
|
|
333
|
+
# Output success message (ASCII-safe for Windows)
|
|
334
|
+
print(f"[OK] Created handoff folder: {handoff_folder}")
|
|
335
|
+
print(f" - index.md (entry point with navigation)")
|
|
336
|
+
|
|
337
|
+
files_created = [f.name for f in handoff_folder.iterdir() if f.is_file() and f.name != 'index.md']
|
|
338
|
+
print(f" - {', '.join(sorted(files_created))}")
|
|
339
|
+
|
|
94
340
|
print()
|
|
95
|
-
print("
|
|
341
|
+
print("Handoff document saved. Use this folder for context in the next session.")
|
|
96
342
|
|
|
97
343
|
|
|
98
344
|
if __name__ == "__main__":
|
|
@@ -52,7 +52,7 @@ If no active context is found, inform the user and stop - handoffs require an ac
|
|
|
52
52
|
|
|
53
53
|
### Step 3: Generate Document
|
|
54
54
|
|
|
55
|
-
Use this template:
|
|
55
|
+
Use this template. The `<!-- SECTION: name -->` markers are required for the save script to parse sections into sharded files.
|
|
56
56
|
|
|
57
57
|
```markdown
|
|
58
58
|
---
|
|
@@ -66,34 +66,40 @@ plan_document: {path to plan if provided, or "none"}
|
|
|
66
66
|
|
|
67
67
|
# Session Handoff — {Date}
|
|
68
68
|
|
|
69
|
+
<!-- SECTION: summary -->
|
|
69
70
|
## Summary
|
|
70
71
|
{2-3 sentences: what's different now vs. session start}
|
|
71
72
|
|
|
73
|
+
<!-- SECTION: completed -->
|
|
72
74
|
## Work Completed
|
|
73
75
|
{Grouped by category if multiple areas. Specific file:function references.}
|
|
74
76
|
|
|
77
|
+
<!-- SECTION: dead-ends -->
|
|
75
78
|
## Dead Ends — Do Not Retry
|
|
76
|
-
{Approaches that were tried and failed. Critical for avoiding wasted effort in future sessions.}
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
| Approach Tried | Why It Failed | Time Spent |
|
|
80
|
-
|----------------|---------------|------------|
|
|
81
|
-
| {What was attempted} | {Specific reason: error, incompatibility, performance, etc.} | {Rough estimate} |
|
|
80
|
+
These approaches were attempted and failed. Do not retry without addressing the root cause.
|
|
82
81
|
|
|
83
|
-
|
|
82
|
+
| Approach | Why It Failed | Time Spent | Alternative |
|
|
83
|
+
|----------|---------------|------------|-------------|
|
|
84
|
+
| {What was attempted} | {Specific reason} | {Rough estimate} | {What to try instead} |
|
|
84
85
|
|
|
86
|
+
<!-- SECTION: decisions -->
|
|
85
87
|
## Key Decisions
|
|
86
88
|
{Technical choices with rationale. Format: **Decision**: Rationale. Trade-off: X.}
|
|
87
89
|
|
|
90
|
+
<!-- SECTION: pending -->
|
|
88
91
|
## Pending Issues
|
|
89
92
|
- [ ] {Issue} — {severity: HIGH/MED/LOW} {optional workaround note}
|
|
90
93
|
|
|
94
|
+
<!-- SECTION: next-steps -->
|
|
91
95
|
## Next Steps
|
|
92
96
|
1. {Actionable item with file:line reference if applicable}
|
|
93
97
|
|
|
98
|
+
<!-- SECTION: files -->
|
|
94
99
|
## Files Modified
|
|
95
100
|
{Significant changes only. Skip formatting-only edits.}
|
|
96
101
|
|
|
102
|
+
<!-- SECTION: context -->
|
|
97
103
|
## Context for Future Sessions
|
|
98
104
|
{Non-obvious context: env quirks, stakeholder requirements}
|
|
99
105
|
|
|
@@ -150,11 +156,12 @@ EOF
|
|
|
150
156
|
```
|
|
151
157
|
|
|
152
158
|
This script:
|
|
153
|
-
1.
|
|
154
|
-
2.
|
|
155
|
-
3.
|
|
159
|
+
1. Creates a folder at `_output/contexts/{context_id}/handoffs/{YYYY-MM-DD-HHMM}/`
|
|
160
|
+
2. Parses sections and writes sharded files (index.md, completed-work.md, dead-ends.md, etc.)
|
|
161
|
+
3. Copies the current plan (if any) to plan.md
|
|
162
|
+
4. Records the event in the context's event log (informational only)
|
|
156
163
|
|
|
157
|
-
|
|
164
|
+
Use the handoff folder for context in the next session.
|
|
158
165
|
|
|
159
166
|
## Dead Ends Section Guidelines
|
|
160
167
|
|
|
@@ -185,10 +192,14 @@ This section is critical for preventing context rot across sessions. Be specific
|
|
|
185
192
|
After creating file, output:
|
|
186
193
|
|
|
187
194
|
```
|
|
188
|
-
✓ Created _output/contexts/{context_id}/handoffs/
|
|
195
|
+
✓ Created handoff folder: _output/contexts/{context_id}/handoffs/{YYYY-MM-DD-HHMM}/
|
|
196
|
+
- index.md (entry point with navigation)
|
|
197
|
+
- completed-work.md, dead-ends.md, decisions.md, pending.md, context.md
|
|
198
|
+
- plan.md (copy of current plan, if any)
|
|
189
199
|
|
|
190
200
|
To continue next session:
|
|
191
|
-
|
|
201
|
+
The index.md will be automatically suggested when you start a new session.
|
|
202
|
+
Read dead-ends.md first to avoid repeating failed approaches.
|
|
192
203
|
|
|
193
204
|
⚠️ {N} dead ends documented — avoid re-attempting these approaches
|
|
194
205
|
```
|
|
@@ -203,10 +214,13 @@ If plan was updated:
|
|
|
203
214
|
|
|
204
215
|
## Success Criteria
|
|
205
216
|
|
|
206
|
-
- [ ] Handoff
|
|
207
|
-
- [ ]
|
|
217
|
+
- [ ] Handoff folder created at `handoffs/{YYYY-MM-DD-HHMM}/`
|
|
218
|
+
- [ ] index.md contains summary and navigation table
|
|
219
|
+
- [ ] All section files created (completed-work.md, dead-ends.md, etc.)
|
|
220
|
+
- [ ] Dead ends use structured table format for quick scanning
|
|
221
|
+
- [ ] plan.md copied from context if plan exists
|
|
208
222
|
- [ ] Next steps are actionable with file references
|
|
209
|
-
- [ ] Git status
|
|
223
|
+
- [ ] Git status included in index.md
|
|
210
224
|
- [ ] If plan provided: checkboxes updated to reflect completion status
|
|
211
225
|
- [ ] If plan provided: Session Progress Log appended
|
|
212
226
|
- [ ] Context state updated to indicate handoff pending
|