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.
Files changed (80) hide show
  1. package/README.md +19 -35
  2. package/dist/lib/template-installer.js +38 -0
  3. package/dist/templates/_shared/.claude/commands/handoff.md +219 -7
  4. package/dist/templates/_shared/.codex/workflows/handoff.md +219 -7
  5. package/dist/templates/_shared/.windsurf/workflows/handoff.md +219 -7
  6. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  7. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  8. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  9. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  10. package/dist/templates/_shared/hooks/context_enforcer.py +73 -19
  11. package/dist/templates/_shared/hooks/context_monitor.py +28 -10
  12. package/dist/templates/_shared/hooks/file-suggestion.py +45 -15
  13. package/dist/templates/_shared/hooks/session_start.py +108 -0
  14. package/dist/templates/_shared/hooks/task_create_atomicity.py +199 -0
  15. package/dist/templates/_shared/hooks/task_create_capture.py +2 -2
  16. package/dist/templates/_shared/hooks/task_update_capture.py +2 -2
  17. package/dist/templates/_shared/hooks/user_prompt_submit.py +58 -23
  18. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  19. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  20. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  21. package/dist/templates/_shared/lib/base/constants.py +45 -0
  22. package/dist/templates/_shared/lib/base/inference.py +29 -21
  23. package/dist/templates/_shared/lib/base/stop_words.py +158 -0
  24. package/dist/templates/_shared/lib/base/subprocess_utils.py +46 -0
  25. package/dist/templates/_shared/lib/base/utils.py +6 -3
  26. package/dist/templates/_shared/lib/context/__init__.py +0 -10
  27. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  28. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  29. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  30. package/dist/templates/_shared/lib/context/cache.py +2 -4
  31. package/dist/templates/_shared/lib/context/context_manager.py +2 -119
  32. package/dist/templates/_shared/lib/context/discovery.py +8 -50
  33. package/dist/templates/_shared/lib/context/task_sync.py +5 -82
  34. package/dist/templates/_shared/lib/handoff/document_generator.py +2 -5
  35. package/dist/templates/_shared/lib/templates/README.md +0 -1
  36. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  37. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  38. package/dist/templates/_shared/lib/templates/formatters.py +0 -1
  39. package/dist/templates/_shared/lib/templates/persona_questions.py +113 -0
  40. package/dist/templates/_shared/lib/templates/plan_context.py +13 -27
  41. package/dist/templates/_shared/scripts/save_handoff.py +289 -43
  42. package/dist/templates/_shared/workflows/handoff.md +30 -16
  43. package/dist/templates/cc-native/.claude/agents/cc-native/ACCESSIBILITY-TESTER.md +1 -1
  44. package/dist/templates/cc-native/.claude/agents/cc-native/ARCHITECT-REVIEWER.md +1 -1
  45. package/dist/templates/cc-native/.claude/agents/cc-native/ASSUMPTION-CHAIN-TRACER.md +1 -1
  46. package/dist/templates/cc-native/.claude/agents/cc-native/CLARITY-AUDITOR.md +1 -1
  47. package/dist/templates/cc-native/.claude/agents/cc-native/CODE-REVIEWER.md +1 -1
  48. package/dist/templates/cc-native/.claude/agents/cc-native/COMPLETENESS-CHECKER.md +1 -1
  49. package/dist/templates/cc-native/.claude/agents/cc-native/CONTEXT-EXTRACTOR.md +1 -1
  50. package/dist/templates/cc-native/.claude/agents/cc-native/DEVILS-ADVOCATE.md +1 -1
  51. package/dist/templates/cc-native/.claude/agents/cc-native/FEASIBILITY-ANALYST.md +1 -1
  52. package/dist/templates/cc-native/.claude/agents/cc-native/FRESH-PERSPECTIVE.md +1 -1
  53. package/dist/templates/cc-native/.claude/agents/cc-native/HANDOFF-READINESS.md +1 -1
  54. package/dist/templates/cc-native/.claude/agents/cc-native/HIDDEN-COMPLEXITY-DETECTOR.md +1 -1
  55. package/dist/templates/cc-native/.claude/agents/cc-native/INCENTIVE-MAPPER.md +1 -1
  56. package/dist/templates/cc-native/.claude/agents/cc-native/PENETRATION-TESTER.md +1 -1
  57. package/dist/templates/cc-native/.claude/agents/cc-native/PERFORMANCE-ENGINEER.md +1 -1
  58. package/dist/templates/cc-native/.claude/agents/cc-native/PLAN-ORCHESTRATOR.md +1 -1
  59. package/dist/templates/cc-native/.claude/agents/cc-native/PRECEDENT-FINDER.md +1 -1
  60. package/dist/templates/cc-native/.claude/agents/cc-native/REVERSIBILITY-ANALYST.md +1 -1
  61. package/dist/templates/cc-native/.claude/agents/cc-native/RISK-ASSESSOR.md +1 -1
  62. package/dist/templates/cc-native/.claude/agents/cc-native/SECOND-ORDER-ANALYST.md +1 -1
  63. package/dist/templates/cc-native/.claude/agents/cc-native/SIMPLICITY-GUARDIAN.md +1 -1
  64. package/dist/templates/cc-native/.claude/agents/cc-native/SKEPTIC.md +1 -1
  65. package/dist/templates/cc-native/.claude/agents/cc-native/STAKEHOLDER-ADVOCATE.md +1 -1
  66. package/dist/templates/cc-native/.claude/agents/cc-native/TRADE-OFF-ILLUMINATOR.md +1 -1
  67. package/dist/templates/cc-native/.claude/settings.json +21 -0
  68. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +211 -0
  69. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  70. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +87 -27
  71. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +240 -0
  72. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  73. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +10 -0
  74. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  75. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +10 -0
  76. package/dist/templates/cc-native/_cc-native/lib/utils.py +123 -10
  77. package/dist/templates/cc-native/_cc-native/plan-review.config.json +6 -0
  78. package/oclif.manifest.json +1 -1
  79. package/package.json +1 -1
  80. package/dist/templates/cc-native/_cc-native/docs/PERMISSION_REQUEST_VERIFICATION.md +0 -147
@@ -11,7 +11,6 @@ MODE_DISPLAY_MAP = {
11
11
  "planning": "[Planning]",
12
12
  "pending_implementation": "[Plan Ready]",
13
13
  "implementing": "[Implementing]",
14
- "handoff_pending": "[Handoff Pending]",
15
14
  "none": "",
16
15
  }
17
16
 
@@ -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
- return """
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 with this question:**
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 interview you about technical details, constraints, and preferences, then update the plan")
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 - Interview them about:
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
- Ask focused, **non-obvious** questions using AskUserQuestion (max 12). Questions should surface hidden constraints, unstated assumptions, or preferences that aren't evident from the original request - things that would meaningfully change the plan.
99
+ {persona_questions}
114
100
 
115
- After gathering answers, **update the plan file** with the refined content before calling ExitPlanMode.
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 and set context status to handoff_pending.
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. Reads handoff markdown content from stdin
14
- 2. Saves it to _output/contexts/{context_id}/handoffs/HANDOFF-{HHMM}.md
15
- 3. Sets in_flight.mode = "handoff_pending"
16
- 4. Records the event in events.jsonl
17
-
18
- The handoff will be automatically picked up by the next session.
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 update_handoff_status, get_context
30
- from lib.base.utils import eprint, atomic_write
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
- # Create handoffs directory
59
- handoffs_dir = project_root / "_output" / "contexts" / context_id / "handoffs"
60
- handoffs_dir.mkdir(parents=True, exist_ok=True)
239
+ # Parse frontmatter and sections
240
+ frontmatter, body = parse_frontmatter(content)
241
+ sections = parse_handoff_sections(body)
61
242
 
62
- # Generate filename with timestamp
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
- # Handle filename collision (add suffix if file exists)
68
- counter = 1
69
- while file_path.exists():
70
- filename = f"HANDOFF-{timestamp}-{counter}.md"
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
- # Save the handoff document
75
- try:
76
- success, error = atomic_write(file_path, content)
77
- if not success:
78
- print(f"Error: Failed to write handoff: {error}", file=sys.stderr)
79
- sys.exit(1)
80
- except Exception as e:
81
- print(f"Error: Failed to write handoff: {e}", file=sys.stderr)
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
- # Update context status
85
- try:
86
- update_handoff_status(context_id, str(file_path), project_root)
87
- except Exception as e:
88
- eprint(f"[save_handoff] Warning: Status update failed: {e}")
89
- # Don't exit - file was saved successfully
90
-
91
- # Output success message
92
- print(f"✓ Saved handoff: {file_path}")
93
- print(f"✓ Set status to handoff_pending for context: {context_id}")
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("The next session will automatically offer to resume from this handoff.")
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
- ### {Problem/Goal attempted}
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
- **What to try instead**: {If known, suggest alternative direction}
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. Saves the handoff to `_output/contexts/{context_id}/handoffs/HANDOFF-{HHMM}.md`
154
- 2. Sets `in_flight.mode = "handoff_pending"`
155
- 3. Records the event in the context's event log
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
- The next session will automatically detect the handoff and offer to resume.
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/HANDOFF-{HHMM}.md
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
- "Load _output/contexts/{context_id}/handoffs/HANDOFF-{HHMM}.md and continue from next steps"
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 document created in context's `handoffs/` subfolder
207
- - [ ] Dead ends section captures all failed approaches with specific details
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 reflects current state
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