anvil-dev-framework 0.1.6
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 +719 -0
- package/VERSION +1 -0
- package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
- package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
- package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
- package/docs/INSTALLATION.md +984 -0
- package/docs/anvil-hud.md +469 -0
- package/docs/anvil-init.md +255 -0
- package/docs/anvil-state.md +210 -0
- package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
- package/docs/command-reference.md +2022 -0
- package/docs/hooks-tts.md +368 -0
- package/docs/implementation-guide.md +810 -0
- package/docs/linear-github-integration.md +247 -0
- package/docs/local-issues.md +677 -0
- package/docs/patterns/README.md +419 -0
- package/docs/planning-responsibilities.md +139 -0
- package/docs/session-workflow.md +573 -0
- package/docs/simplification-plan-template.md +297 -0
- package/docs/simplification-principles.md +129 -0
- package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
- package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
- package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
- package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
- package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
- package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
- package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
- package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
- package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
- package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
- package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
- package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
- package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
- package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
- package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
- package/docs/sync.md +122 -0
- package/global/CLAUDE.md +140 -0
- package/global/agents/verify-app.md +164 -0
- package/global/commands/anvil-settings.md +527 -0
- package/global/commands/anvil-sync.md +121 -0
- package/global/commands/change.md +197 -0
- package/global/commands/clarify.md +252 -0
- package/global/commands/cleanup.md +292 -0
- package/global/commands/commit-push-pr.md +207 -0
- package/global/commands/decay-review.md +127 -0
- package/global/commands/discover.md +158 -0
- package/global/commands/doc-coverage.md +122 -0
- package/global/commands/evidence.md +307 -0
- package/global/commands/explore.md +121 -0
- package/global/commands/force-exit.md +135 -0
- package/global/commands/handoff.md +191 -0
- package/global/commands/healthcheck.md +302 -0
- package/global/commands/hud.md +84 -0
- package/global/commands/insights.md +319 -0
- package/global/commands/linear-setup.md +184 -0
- package/global/commands/lint-fix.md +198 -0
- package/global/commands/orient.md +510 -0
- package/global/commands/plan.md +228 -0
- package/global/commands/ralph.md +346 -0
- package/global/commands/ready.md +182 -0
- package/global/commands/release.md +305 -0
- package/global/commands/retro.md +96 -0
- package/global/commands/shard.md +166 -0
- package/global/commands/spec.md +227 -0
- package/global/commands/sprint.md +184 -0
- package/global/commands/tasks.md +228 -0
- package/global/commands/test-and-commit.md +151 -0
- package/global/commands/validate.md +132 -0
- package/global/commands/verify.md +251 -0
- package/global/commands/weekly-review.md +156 -0
- package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
- package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
- package/global/hooks/anvil_memory_observe.ts +322 -0
- package/global/hooks/anvil_memory_session.ts +166 -0
- package/global/hooks/anvil_memory_stop.ts +187 -0
- package/global/hooks/parse_transcript.py +116 -0
- package/global/hooks/post_merge_cleanup.sh +132 -0
- package/global/hooks/post_tool_format.sh +215 -0
- package/global/hooks/ralph_context_monitor.py +240 -0
- package/global/hooks/ralph_stop.sh +502 -0
- package/global/hooks/statusline.sh +1110 -0
- package/global/hooks/statusline_agent_sync.py +224 -0
- package/global/hooks/stop_gate.sh +250 -0
- package/global/lib/.claude/anvil-state.json +21 -0
- package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
- package/global/lib/agent_registry.py +995 -0
- package/global/lib/anvil-state.sh +435 -0
- package/global/lib/claim_service.py +515 -0
- package/global/lib/coderabbit_service.py +314 -0
- package/global/lib/config_service.py +423 -0
- package/global/lib/coordination_service.py +331 -0
- package/global/lib/doc_coverage_service.py +1305 -0
- package/global/lib/gate_logger.py +316 -0
- package/global/lib/github_service.py +310 -0
- package/global/lib/handoff_generator.py +775 -0
- package/global/lib/hygiene_service.py +712 -0
- package/global/lib/issue_models.py +257 -0
- package/global/lib/issue_provider.py +339 -0
- package/global/lib/linear_data_service.py +210 -0
- package/global/lib/linear_provider.py +987 -0
- package/global/lib/linear_provider.py.backup +671 -0
- package/global/lib/local_provider.py +486 -0
- package/global/lib/orient_fast.py +457 -0
- package/global/lib/quality_service.py +470 -0
- package/global/lib/ralph_prompt_generator.py +563 -0
- package/global/lib/ralph_state.py +1202 -0
- package/global/lib/state_manager.py +417 -0
- package/global/lib/transcript_parser.py +597 -0
- package/global/lib/verification_runner.py +557 -0
- package/global/lib/verify_iteration.py +490 -0
- package/global/lib/verify_subagent.py +250 -0
- package/global/skills/README.md +155 -0
- package/global/skills/quality-gates/SKILL.md +252 -0
- package/global/skills/skill-template/SKILL.md +109 -0
- package/global/skills/testing-strategies/SKILL.md +337 -0
- package/global/templates/CHANGE-template.md +105 -0
- package/global/templates/HANDOFF-template.md +63 -0
- package/global/templates/PLAN-template.md +111 -0
- package/global/templates/SPEC-template.md +93 -0
- package/global/templates/ralph/PROMPT.md.template +89 -0
- package/global/templates/ralph/fix_plan.md.template +31 -0
- package/global/templates/ralph/progress.txt.template +23 -0
- package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
- package/global/tests/test_doc_coverage.py +520 -0
- package/global/tests/test_issue_models.py +299 -0
- package/global/tests/test_local_provider.py +323 -0
- package/global/tools/README.md +178 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +3622 -0
- package/global/tools/anvil-hud.py.bak +3318 -0
- package/global/tools/anvil-issue.py +432 -0
- package/global/tools/anvil-memory/CLAUDE.md +49 -0
- package/global/tools/anvil-memory/README.md +42 -0
- package/global/tools/anvil-memory/bun.lock +25 -0
- package/global/tools/anvil-memory/bunfig.toml +9 -0
- package/global/tools/anvil-memory/package.json +23 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
- package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
- package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
- package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
- package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
- package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
- package/global/tools/anvil-memory/src/commands/get.ts +115 -0
- package/global/tools/anvil-memory/src/commands/init.ts +94 -0
- package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
- package/global/tools/anvil-memory/src/commands/search.ts +112 -0
- package/global/tools/anvil-memory/src/db.ts +638 -0
- package/global/tools/anvil-memory/src/index.ts +205 -0
- package/global/tools/anvil-memory/src/types.ts +122 -0
- package/global/tools/anvil-memory/tsconfig.json +29 -0
- package/global/tools/ralph-loop.sh +359 -0
- package/package.json +45 -0
- package/scripts/anvil +822 -0
- package/scripts/extract_patterns.py +222 -0
- package/scripts/init-project.sh +541 -0
- package/scripts/install.sh +229 -0
- package/scripts/postinstall.js +41 -0
- package/scripts/rollback.sh +188 -0
- package/scripts/sync.sh +623 -0
- package/scripts/test-statusline.sh +248 -0
- package/scripts/update_claude_md.py +224 -0
- package/scripts/verify.sh +255 -0
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
handoff_generator.py - CCS Handoff Document Generator (ANV-195)
|
|
4
|
+
|
|
5
|
+
Generates structured handoff documents for checkpoint continuity.
|
|
6
|
+
Collects session state (git, Linear, files) and generates markdown
|
|
7
|
+
documents with checkpoint metadata and resume instructions.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from global.lib.handoff_generator import HandoffGenerator
|
|
11
|
+
|
|
12
|
+
# Generate handoff document
|
|
13
|
+
generator = HandoffGenerator(project_path="/path/to/project")
|
|
14
|
+
handoff = generator.generate(
|
|
15
|
+
trigger_level="L2",
|
|
16
|
+
context_percent=85,
|
|
17
|
+
linear_issue="ANV-123",
|
|
18
|
+
summary="Implementing feature X"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Save to file
|
|
22
|
+
filepath = generator.save(handoff)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import subprocess
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from typing import List, Optional, TypedDict
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# =============================================================================
|
|
34
|
+
# Configuration
|
|
35
|
+
# =============================================================================
|
|
36
|
+
|
|
37
|
+
DEFAULT_HANDOFF_DIR = ".claude/handoffs"
|
|
38
|
+
CHECKPOINT_LEVELS = {"L1": "Warning", "L2": "Critical", "L3": "Emergency"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# =============================================================================
|
|
42
|
+
# Type Definitions
|
|
43
|
+
# =============================================================================
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GitState(TypedDict, total=False):
|
|
47
|
+
"""Git repository state."""
|
|
48
|
+
|
|
49
|
+
branch: str
|
|
50
|
+
commit_hash: str
|
|
51
|
+
commit_message: str
|
|
52
|
+
has_uncommitted: bool
|
|
53
|
+
staged_files: List[str]
|
|
54
|
+
modified_files: List[str]
|
|
55
|
+
untracked_files: List[str]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class LinearState(TypedDict, total=False):
|
|
59
|
+
"""Linear issue state."""
|
|
60
|
+
|
|
61
|
+
identifier: str
|
|
62
|
+
title: str
|
|
63
|
+
state: str
|
|
64
|
+
description: str
|
|
65
|
+
assignee: Optional[str]
|
|
66
|
+
url: str
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class FileProgress(TypedDict, total=False):
|
|
70
|
+
"""File modification progress."""
|
|
71
|
+
|
|
72
|
+
path: str
|
|
73
|
+
lines_changed: int
|
|
74
|
+
status: str # added, modified, deleted
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class HandoffDocument(TypedDict):
|
|
78
|
+
"""Complete handoff document structure."""
|
|
79
|
+
|
|
80
|
+
# Metadata (YAML frontmatter)
|
|
81
|
+
session_date: str
|
|
82
|
+
session_time: str
|
|
83
|
+
branch: str
|
|
84
|
+
linear_issues: str
|
|
85
|
+
checkpoint_trigger: str
|
|
86
|
+
context_at_checkpoint: int
|
|
87
|
+
|
|
88
|
+
# Content sections
|
|
89
|
+
title: str
|
|
90
|
+
summary: str
|
|
91
|
+
completed_items: List[str]
|
|
92
|
+
in_progress_items: List[str]
|
|
93
|
+
next_steps: List[str]
|
|
94
|
+
git_state: GitState
|
|
95
|
+
linear_state: Optional[LinearState]
|
|
96
|
+
files_touched: List[FileProgress]
|
|
97
|
+
critical_context: List[str]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# =============================================================================
|
|
101
|
+
# HandoffGenerator Class
|
|
102
|
+
# =============================================================================
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class HandoffGenerator:
|
|
107
|
+
"""Generate handoff documents for CCS checkpoints (ANV-195)."""
|
|
108
|
+
|
|
109
|
+
project_path: str = field(default_factory=os.getcwd)
|
|
110
|
+
handoff_dir: str = DEFAULT_HANDOFF_DIR
|
|
111
|
+
|
|
112
|
+
def __post_init__(self):
|
|
113
|
+
"""Ensure project path is absolute."""
|
|
114
|
+
self.project_path = os.path.abspath(self.project_path)
|
|
115
|
+
|
|
116
|
+
# -------------------------------------------------------------------------
|
|
117
|
+
# Git State Collection
|
|
118
|
+
# -------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def get_git_state(self) -> GitState:
|
|
121
|
+
"""Collect current git repository state."""
|
|
122
|
+
state: GitState = {
|
|
123
|
+
"branch": "",
|
|
124
|
+
"commit_hash": "",
|
|
125
|
+
"commit_message": "",
|
|
126
|
+
"has_uncommitted": False,
|
|
127
|
+
"staged_files": [],
|
|
128
|
+
"modified_files": [],
|
|
129
|
+
"untracked_files": [],
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
# Get current branch
|
|
134
|
+
result = subprocess.run(
|
|
135
|
+
["git", "branch", "--show-current"],
|
|
136
|
+
capture_output=True,
|
|
137
|
+
text=True,
|
|
138
|
+
cwd=self.project_path,
|
|
139
|
+
timeout=5,
|
|
140
|
+
)
|
|
141
|
+
if result.returncode == 0:
|
|
142
|
+
state["branch"] = result.stdout.strip()
|
|
143
|
+
|
|
144
|
+
# Get commit hash and message
|
|
145
|
+
result = subprocess.run(
|
|
146
|
+
["git", "log", "-1", "--format=%H|%s"],
|
|
147
|
+
capture_output=True,
|
|
148
|
+
text=True,
|
|
149
|
+
cwd=self.project_path,
|
|
150
|
+
timeout=5,
|
|
151
|
+
)
|
|
152
|
+
if result.returncode == 0 and "|" in result.stdout:
|
|
153
|
+
parts = result.stdout.strip().split("|", 1)
|
|
154
|
+
state["commit_hash"] = parts[0][:8] # Short hash
|
|
155
|
+
state["commit_message"] = parts[1] if len(parts) > 1 else ""
|
|
156
|
+
|
|
157
|
+
# Get staged files
|
|
158
|
+
result = subprocess.run(
|
|
159
|
+
["git", "diff", "--name-only", "--cached"],
|
|
160
|
+
capture_output=True,
|
|
161
|
+
text=True,
|
|
162
|
+
cwd=self.project_path,
|
|
163
|
+
timeout=5,
|
|
164
|
+
)
|
|
165
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
166
|
+
state["staged_files"] = result.stdout.strip().split("\n")
|
|
167
|
+
|
|
168
|
+
# Get modified files (not staged)
|
|
169
|
+
result = subprocess.run(
|
|
170
|
+
["git", "diff", "--name-only"],
|
|
171
|
+
capture_output=True,
|
|
172
|
+
text=True,
|
|
173
|
+
cwd=self.project_path,
|
|
174
|
+
timeout=5,
|
|
175
|
+
)
|
|
176
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
177
|
+
state["modified_files"] = result.stdout.strip().split("\n")
|
|
178
|
+
|
|
179
|
+
# Get untracked files
|
|
180
|
+
result = subprocess.run(
|
|
181
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
182
|
+
capture_output=True,
|
|
183
|
+
text=True,
|
|
184
|
+
cwd=self.project_path,
|
|
185
|
+
timeout=5,
|
|
186
|
+
)
|
|
187
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
188
|
+
state["untracked_files"] = result.stdout.strip().split("\n")
|
|
189
|
+
|
|
190
|
+
# Determine if there are uncommitted changes
|
|
191
|
+
state["has_uncommitted"] = bool(
|
|
192
|
+
state["staged_files"]
|
|
193
|
+
or state["modified_files"]
|
|
194
|
+
or state["untracked_files"]
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError):
|
|
198
|
+
pass # Return partial state on error
|
|
199
|
+
|
|
200
|
+
return state
|
|
201
|
+
|
|
202
|
+
def get_file_progress(self) -> List[FileProgress]:
|
|
203
|
+
"""Get detailed file modification info."""
|
|
204
|
+
files: List[FileProgress] = []
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
# Get diff stats for staged and unstaged
|
|
208
|
+
result = subprocess.run(
|
|
209
|
+
["git", "diff", "--stat", "--numstat", "HEAD"],
|
|
210
|
+
capture_output=True,
|
|
211
|
+
text=True,
|
|
212
|
+
cwd=self.project_path,
|
|
213
|
+
timeout=10,
|
|
214
|
+
)
|
|
215
|
+
if result.returncode == 0:
|
|
216
|
+
for line in result.stdout.strip().split("\n"):
|
|
217
|
+
if not line or "\t" not in line:
|
|
218
|
+
continue
|
|
219
|
+
parts = line.split("\t")
|
|
220
|
+
if len(parts) >= 3:
|
|
221
|
+
added = int(parts[0]) if parts[0].isdigit() else 0
|
|
222
|
+
deleted = int(parts[1]) if parts[1].isdigit() else 0
|
|
223
|
+
filepath = parts[2]
|
|
224
|
+
files.append(
|
|
225
|
+
{
|
|
226
|
+
"path": filepath,
|
|
227
|
+
"lines_changed": added + deleted,
|
|
228
|
+
"status": "modified",
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError):
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
return files
|
|
236
|
+
|
|
237
|
+
# -------------------------------------------------------------------------
|
|
238
|
+
# Linear State Collection
|
|
239
|
+
# -------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
def get_linear_state(self, issue_id: str) -> Optional[LinearState]:
|
|
242
|
+
"""Fetch Linear issue state using the linear.py script."""
|
|
243
|
+
if not issue_id:
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
# Use the linear.py script from the linear-skill
|
|
248
|
+
script_path = os.path.expanduser(
|
|
249
|
+
"~/.claude/skills/linear-skill/scripts/linear.py"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if not os.path.exists(script_path):
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
result = subprocess.run(
|
|
256
|
+
["python3", script_path, "get-issue", "--id", issue_id],
|
|
257
|
+
capture_output=True,
|
|
258
|
+
text=True,
|
|
259
|
+
timeout=10,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if result.returncode == 0:
|
|
263
|
+
data = json.loads(result.stdout)
|
|
264
|
+
assignee_data = data.get("assignee")
|
|
265
|
+
assignee_name = assignee_data.get("name") if assignee_data else None
|
|
266
|
+
state_data = data.get("state")
|
|
267
|
+
state_name = state_data.get("name", "Unknown") if state_data else "Unknown"
|
|
268
|
+
description = data.get("description") or ""
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
"identifier": data.get("identifier", issue_id),
|
|
272
|
+
"title": data.get("title", ""),
|
|
273
|
+
"state": state_name,
|
|
274
|
+
"description": description[:200], # Truncate
|
|
275
|
+
"assignee": assignee_name,
|
|
276
|
+
"url": data.get("url", ""),
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError, json.JSONDecodeError):
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
return {"identifier": issue_id, "title": "", "state": "Unknown"}
|
|
283
|
+
|
|
284
|
+
# -------------------------------------------------------------------------
|
|
285
|
+
# Document Generation
|
|
286
|
+
# -------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
def generate(
|
|
289
|
+
self,
|
|
290
|
+
trigger_level: str = "L2",
|
|
291
|
+
context_percent: int = 0,
|
|
292
|
+
linear_issue: str = "",
|
|
293
|
+
summary: str = "",
|
|
294
|
+
completed_items: Optional[List[str]] = None,
|
|
295
|
+
in_progress_items: Optional[List[str]] = None,
|
|
296
|
+
next_steps: Optional[List[str]] = None,
|
|
297
|
+
critical_context: Optional[List[str]] = None,
|
|
298
|
+
) -> HandoffDocument:
|
|
299
|
+
"""Generate a complete handoff document.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
trigger_level: CCS level that triggered checkpoint (L1, L2, L3)
|
|
303
|
+
context_percent: Context percentage at checkpoint
|
|
304
|
+
linear_issue: Linear issue ID (e.g., "ANV-195")
|
|
305
|
+
summary: Brief summary of work done
|
|
306
|
+
completed_items: List of completed tasks
|
|
307
|
+
in_progress_items: List of in-progress tasks
|
|
308
|
+
next_steps: Recommended next actions
|
|
309
|
+
critical_context: Important context for resumption
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
HandoffDocument with all sections populated
|
|
313
|
+
"""
|
|
314
|
+
now = datetime.now(timezone.utc)
|
|
315
|
+
git_state = self.get_git_state()
|
|
316
|
+
linear_state = self.get_linear_state(linear_issue) if linear_issue else None
|
|
317
|
+
files_touched = self.get_file_progress()
|
|
318
|
+
|
|
319
|
+
# Generate title
|
|
320
|
+
level_desc = CHECKPOINT_LEVELS.get(trigger_level, "Checkpoint")
|
|
321
|
+
if linear_state and linear_state.get("title"):
|
|
322
|
+
title = f"Session Handoff: {linear_state['identifier']} - {linear_state['title']}"
|
|
323
|
+
elif summary:
|
|
324
|
+
title = f"Session Handoff: {summary[:50]}"
|
|
325
|
+
else:
|
|
326
|
+
title = f"Session Handoff: {level_desc} Checkpoint"
|
|
327
|
+
|
|
328
|
+
# Generate default next steps if not provided
|
|
329
|
+
if not next_steps:
|
|
330
|
+
next_steps = self._generate_default_next_steps(
|
|
331
|
+
trigger_level, git_state, linear_issue
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
# Metadata
|
|
336
|
+
"session_date": now.strftime("%Y-%m-%d"),
|
|
337
|
+
"session_time": now.strftime("%H:%M"),
|
|
338
|
+
"branch": git_state.get("branch", "unknown"),
|
|
339
|
+
"linear_issues": linear_issue,
|
|
340
|
+
"checkpoint_trigger": f"{trigger_level} (context at {context_percent}%)",
|
|
341
|
+
"context_at_checkpoint": context_percent,
|
|
342
|
+
# Content
|
|
343
|
+
"title": title,
|
|
344
|
+
"summary": summary,
|
|
345
|
+
"completed_items": completed_items or [],
|
|
346
|
+
"in_progress_items": in_progress_items or [],
|
|
347
|
+
"next_steps": next_steps,
|
|
348
|
+
"git_state": git_state,
|
|
349
|
+
"linear_state": linear_state,
|
|
350
|
+
"files_touched": files_touched,
|
|
351
|
+
"critical_context": critical_context or [],
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
def _generate_default_next_steps(
|
|
355
|
+
self, trigger_level: str, git_state: GitState, linear_issue: str
|
|
356
|
+
) -> List[str]:
|
|
357
|
+
"""Generate default next steps based on state."""
|
|
358
|
+
steps = []
|
|
359
|
+
|
|
360
|
+
# Context-level specific guidance
|
|
361
|
+
if trigger_level == "L3":
|
|
362
|
+
steps.append("URGENT: Review handoff immediately - context was near limit")
|
|
363
|
+
elif trigger_level == "L2":
|
|
364
|
+
steps.append("Review handoff and continue from in-progress items")
|
|
365
|
+
else:
|
|
366
|
+
steps.append("Continue from where the session left off")
|
|
367
|
+
|
|
368
|
+
# Git-based suggestions
|
|
369
|
+
if git_state.get("has_uncommitted"):
|
|
370
|
+
uncommitted_count = (
|
|
371
|
+
len(git_state.get("staged_files", []))
|
|
372
|
+
+ len(git_state.get("modified_files", []))
|
|
373
|
+
)
|
|
374
|
+
steps.append(
|
|
375
|
+
f"Review {uncommitted_count} uncommitted file(s) before continuing"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Linear-based suggestions
|
|
379
|
+
if linear_issue:
|
|
380
|
+
steps.append(f"Check Linear issue {linear_issue} for latest status")
|
|
381
|
+
|
|
382
|
+
return steps
|
|
383
|
+
|
|
384
|
+
# -------------------------------------------------------------------------
|
|
385
|
+
# Markdown Rendering
|
|
386
|
+
# -------------------------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
def render_markdown(self, doc: HandoffDocument) -> str:
|
|
389
|
+
"""Render handoff document as markdown."""
|
|
390
|
+
lines = []
|
|
391
|
+
|
|
392
|
+
# YAML frontmatter
|
|
393
|
+
lines.append("---")
|
|
394
|
+
lines.append(f"session_date: {doc['session_date']}")
|
|
395
|
+
lines.append(f"session_time: {doc['session_time']}")
|
|
396
|
+
lines.append(f"branch: {doc['branch']}")
|
|
397
|
+
if doc["linear_issues"]:
|
|
398
|
+
lines.append(f"linear_issues: {doc['linear_issues']}")
|
|
399
|
+
lines.append(f"checkpoint_trigger: {doc['checkpoint_trigger']}")
|
|
400
|
+
lines.append(f"context_at_checkpoint: {doc['context_at_checkpoint']}%")
|
|
401
|
+
lines.append("---")
|
|
402
|
+
lines.append("")
|
|
403
|
+
|
|
404
|
+
# Title
|
|
405
|
+
lines.append(f"# {doc['title']}")
|
|
406
|
+
lines.append("")
|
|
407
|
+
|
|
408
|
+
# Summary
|
|
409
|
+
if doc["summary"]:
|
|
410
|
+
lines.append("## Session Summary")
|
|
411
|
+
lines.append(doc["summary"])
|
|
412
|
+
lines.append("")
|
|
413
|
+
|
|
414
|
+
# Checkpoint Reason
|
|
415
|
+
lines.append("## Checkpoint Reason")
|
|
416
|
+
lines.append(f"Context checkpoint triggered at {doc['checkpoint_trigger']}")
|
|
417
|
+
lines.append("")
|
|
418
|
+
|
|
419
|
+
# Completed items
|
|
420
|
+
if doc["completed_items"]:
|
|
421
|
+
lines.append("## Completed This Session")
|
|
422
|
+
for item in doc["completed_items"]:
|
|
423
|
+
lines.append(f"- [x] {item}")
|
|
424
|
+
lines.append("")
|
|
425
|
+
|
|
426
|
+
# In progress items
|
|
427
|
+
if doc["in_progress_items"]:
|
|
428
|
+
lines.append("## In Progress (Not Complete)")
|
|
429
|
+
for item in doc["in_progress_items"]:
|
|
430
|
+
lines.append(f"- [ ] {item}")
|
|
431
|
+
lines.append("")
|
|
432
|
+
|
|
433
|
+
# Git State
|
|
434
|
+
git = doc["git_state"]
|
|
435
|
+
lines.append("## Git State")
|
|
436
|
+
lines.append(f"- **Branch**: {git.get('branch', 'unknown')}")
|
|
437
|
+
lines.append(f"- **Last commit**: {git.get('commit_hash', '')} {git.get('commit_message', '')}")
|
|
438
|
+
lines.append(f"- **Uncommitted changes**: {'Yes' if git.get('has_uncommitted') else 'No'}")
|
|
439
|
+
|
|
440
|
+
if git.get("staged_files"):
|
|
441
|
+
lines.append(f"- **Staged files**: {', '.join(git['staged_files'][:5])}")
|
|
442
|
+
if len(git["staged_files"]) > 5:
|
|
443
|
+
lines.append(f" - ...and {len(git['staged_files']) - 5} more")
|
|
444
|
+
|
|
445
|
+
if git.get("modified_files"):
|
|
446
|
+
lines.append(f"- **Modified files**: {', '.join(git['modified_files'][:5])}")
|
|
447
|
+
if len(git["modified_files"]) > 5:
|
|
448
|
+
lines.append(f" - ...and {len(git['modified_files']) - 5} more")
|
|
449
|
+
lines.append("")
|
|
450
|
+
|
|
451
|
+
# Linear State
|
|
452
|
+
if doc["linear_state"]:
|
|
453
|
+
linear = doc["linear_state"]
|
|
454
|
+
lines.append("## Linear Issue State")
|
|
455
|
+
lines.append(f"- **Issue**: {linear.get('identifier', 'Unknown')}")
|
|
456
|
+
lines.append(f"- **Title**: {linear.get('title', 'Unknown')}")
|
|
457
|
+
lines.append(f"- **Status**: {linear.get('state', 'Unknown')}")
|
|
458
|
+
if linear.get("url"):
|
|
459
|
+
lines.append(f"- **URL**: {linear['url']}")
|
|
460
|
+
lines.append("")
|
|
461
|
+
|
|
462
|
+
# Files touched with progress
|
|
463
|
+
if doc["files_touched"]:
|
|
464
|
+
lines.append("## Files Modified")
|
|
465
|
+
for f in doc["files_touched"][:10]:
|
|
466
|
+
lines.append(f"- `{f['path']}` ({f['lines_changed']} lines)")
|
|
467
|
+
if len(doc["files_touched"]) > 10:
|
|
468
|
+
lines.append(f"- ...and {len(doc['files_touched']) - 10} more files")
|
|
469
|
+
lines.append("")
|
|
470
|
+
|
|
471
|
+
# Critical context
|
|
472
|
+
if doc["critical_context"]:
|
|
473
|
+
lines.append("## Critical Context for Next Session")
|
|
474
|
+
for ctx in doc["critical_context"]:
|
|
475
|
+
lines.append(f"- {ctx}")
|
|
476
|
+
lines.append("")
|
|
477
|
+
|
|
478
|
+
# Next steps
|
|
479
|
+
if doc["next_steps"]:
|
|
480
|
+
lines.append("## Recommended Next Steps")
|
|
481
|
+
for i, step in enumerate(doc["next_steps"], 1):
|
|
482
|
+
lines.append(f"{i}. {step}")
|
|
483
|
+
lines.append("")
|
|
484
|
+
|
|
485
|
+
return "\n".join(lines)
|
|
486
|
+
|
|
487
|
+
# -------------------------------------------------------------------------
|
|
488
|
+
# File Operations
|
|
489
|
+
# -------------------------------------------------------------------------
|
|
490
|
+
|
|
491
|
+
def save(self, doc: HandoffDocument, filename: Optional[str] = None) -> str:
|
|
492
|
+
"""Save handoff document to file.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
doc: HandoffDocument to save
|
|
496
|
+
filename: Optional filename (auto-generated if not provided)
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
Path to saved file
|
|
500
|
+
"""
|
|
501
|
+
# Ensure handoff directory exists
|
|
502
|
+
handoff_path = os.path.join(self.project_path, self.handoff_dir)
|
|
503
|
+
os.makedirs(handoff_path, exist_ok=True)
|
|
504
|
+
|
|
505
|
+
# Generate filename if not provided
|
|
506
|
+
if not filename:
|
|
507
|
+
filename = f"{doc['session_date']}-{doc['session_time'].replace(':', '')}.md"
|
|
508
|
+
|
|
509
|
+
filepath = os.path.join(handoff_path, filename)
|
|
510
|
+
|
|
511
|
+
# Render and save
|
|
512
|
+
content = self.render_markdown(doc)
|
|
513
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
514
|
+
f.write(content)
|
|
515
|
+
|
|
516
|
+
return filepath
|
|
517
|
+
|
|
518
|
+
def create_wip_commit(
|
|
519
|
+
self,
|
|
520
|
+
trigger_level: str = "L2",
|
|
521
|
+
context_percent: int = 0,
|
|
522
|
+
linear_issue: str = "",
|
|
523
|
+
) -> tuple[bool, str]:
|
|
524
|
+
"""Create a WIP commit at checkpoint if there are uncommitted changes.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
trigger_level: CCS level that triggered checkpoint (L1, L2, L3)
|
|
528
|
+
context_percent: Context percentage at checkpoint
|
|
529
|
+
linear_issue: Linear issue ID (e.g., "ANV-195")
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
Tuple of (success, message) where message is commit hash or error
|
|
533
|
+
"""
|
|
534
|
+
try:
|
|
535
|
+
# Check if there are uncommitted changes
|
|
536
|
+
result = subprocess.run(
|
|
537
|
+
["git", "status", "--porcelain"],
|
|
538
|
+
capture_output=True,
|
|
539
|
+
text=True,
|
|
540
|
+
cwd=self.project_path,
|
|
541
|
+
timeout=5,
|
|
542
|
+
)
|
|
543
|
+
if result.returncode != 0:
|
|
544
|
+
return False, "Failed to check git status"
|
|
545
|
+
|
|
546
|
+
if not result.stdout.strip():
|
|
547
|
+
return False, "No uncommitted changes to commit"
|
|
548
|
+
|
|
549
|
+
# Stage all changes
|
|
550
|
+
result = subprocess.run(
|
|
551
|
+
["git", "add", "-A"],
|
|
552
|
+
capture_output=True,
|
|
553
|
+
text=True,
|
|
554
|
+
cwd=self.project_path,
|
|
555
|
+
timeout=10,
|
|
556
|
+
)
|
|
557
|
+
if result.returncode != 0:
|
|
558
|
+
return False, f"Failed to stage changes: {result.stderr}"
|
|
559
|
+
|
|
560
|
+
# Build commit message
|
|
561
|
+
issue_part = f"{linear_issue} - " if linear_issue else ""
|
|
562
|
+
commit_msg = f"[WIP] {issue_part}checkpoint ({trigger_level} at {context_percent}%)"
|
|
563
|
+
|
|
564
|
+
# Create the commit
|
|
565
|
+
result = subprocess.run(
|
|
566
|
+
["git", "commit", "-m", commit_msg],
|
|
567
|
+
capture_output=True,
|
|
568
|
+
text=True,
|
|
569
|
+
cwd=self.project_path,
|
|
570
|
+
timeout=30,
|
|
571
|
+
)
|
|
572
|
+
if result.returncode != 0:
|
|
573
|
+
return False, f"Failed to create commit: {result.stderr}"
|
|
574
|
+
|
|
575
|
+
# Get the commit hash
|
|
576
|
+
result = subprocess.run(
|
|
577
|
+
["git", "rev-parse", "--short", "HEAD"],
|
|
578
|
+
capture_output=True,
|
|
579
|
+
text=True,
|
|
580
|
+
cwd=self.project_path,
|
|
581
|
+
timeout=5,
|
|
582
|
+
)
|
|
583
|
+
if result.returncode == 0:
|
|
584
|
+
commit_hash = result.stdout.strip()
|
|
585
|
+
return True, commit_hash
|
|
586
|
+
|
|
587
|
+
return True, "Commit created (hash unknown)"
|
|
588
|
+
|
|
589
|
+
except subprocess.TimeoutExpired:
|
|
590
|
+
return False, "Git command timed out"
|
|
591
|
+
except subprocess.SubprocessError as e:
|
|
592
|
+
return False, f"Git error: {e}"
|
|
593
|
+
|
|
594
|
+
def create_checkpoint_comment(
|
|
595
|
+
self,
|
|
596
|
+
linear_issue: str,
|
|
597
|
+
trigger_level: str = "L2",
|
|
598
|
+
context_percent: int = 0,
|
|
599
|
+
handoff_file: str = "",
|
|
600
|
+
completed_items: Optional[List[str]] = None,
|
|
601
|
+
in_progress_items: Optional[List[str]] = None,
|
|
602
|
+
) -> tuple[bool, str]:
|
|
603
|
+
"""Create a checkpoint comment on a Linear issue.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
linear_issue: Linear issue ID (e.g., "ANV-197")
|
|
607
|
+
trigger_level: CCS level that triggered checkpoint (L1, L2, L3)
|
|
608
|
+
context_percent: Context percentage at checkpoint
|
|
609
|
+
handoff_file: Path to handoff document (relative to project)
|
|
610
|
+
completed_items: List of completed tasks
|
|
611
|
+
in_progress_items: List of in-progress tasks
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
Tuple of (success, message) where message is comment ID or error
|
|
615
|
+
"""
|
|
616
|
+
if not linear_issue:
|
|
617
|
+
return False, "No Linear issue specified"
|
|
618
|
+
|
|
619
|
+
# Build comment body
|
|
620
|
+
level_desc = CHECKPOINT_LEVELS.get(trigger_level, "Checkpoint")
|
|
621
|
+
lines = [
|
|
622
|
+
f"## 🔶 Context Checkpoint ({trigger_level})",
|
|
623
|
+
"",
|
|
624
|
+
f"**Trigger**: {level_desc} at {context_percent}% context usage",
|
|
625
|
+
f"**Timestamp**: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}",
|
|
626
|
+
]
|
|
627
|
+
|
|
628
|
+
# Add handoff file reference
|
|
629
|
+
if handoff_file:
|
|
630
|
+
lines.append(f"**Handoff**: `{handoff_file}`")
|
|
631
|
+
|
|
632
|
+
# Add git state summary
|
|
633
|
+
git_state = self.get_git_state()
|
|
634
|
+
if git_state.get("branch"):
|
|
635
|
+
lines.append(f"**Branch**: `{git_state['branch']}`")
|
|
636
|
+
if git_state.get("commit_hash"):
|
|
637
|
+
lines.append(
|
|
638
|
+
f"**Last Commit**: `{git_state['commit_hash']}` "
|
|
639
|
+
f"{git_state.get('commit_message', '')}"
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
# Add progress summary
|
|
643
|
+
if completed_items:
|
|
644
|
+
lines.append("")
|
|
645
|
+
lines.append("### Completed")
|
|
646
|
+
for item in completed_items[:5]: # Limit to 5 items
|
|
647
|
+
lines.append(f"- [x] {item}")
|
|
648
|
+
if len(completed_items) > 5:
|
|
649
|
+
lines.append(f"- ...and {len(completed_items) - 5} more")
|
|
650
|
+
|
|
651
|
+
if in_progress_items:
|
|
652
|
+
lines.append("")
|
|
653
|
+
lines.append("### In Progress")
|
|
654
|
+
for item in in_progress_items[:5]: # Limit to 5 items
|
|
655
|
+
lines.append(f"- [ ] {item}")
|
|
656
|
+
if len(in_progress_items) > 5:
|
|
657
|
+
lines.append(f"- ...and {len(in_progress_items) - 5} more")
|
|
658
|
+
|
|
659
|
+
# Add resume instructions
|
|
660
|
+
lines.extend(
|
|
661
|
+
[
|
|
662
|
+
"",
|
|
663
|
+
"---",
|
|
664
|
+
"*Session checkpointed due to context limits. "
|
|
665
|
+
"Resume with handoff document.*",
|
|
666
|
+
]
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
comment_body = "\n".join(lines)
|
|
670
|
+
|
|
671
|
+
# Use linear.py script to create comment
|
|
672
|
+
try:
|
|
673
|
+
script_path = os.path.expanduser(
|
|
674
|
+
"~/.claude/skills/linear-skill/scripts/linear.py"
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
if not os.path.exists(script_path):
|
|
678
|
+
return False, "Linear skill script not found"
|
|
679
|
+
|
|
680
|
+
# Create comment via Linear API
|
|
681
|
+
result = subprocess.run(
|
|
682
|
+
[
|
|
683
|
+
"python3",
|
|
684
|
+
script_path,
|
|
685
|
+
"create-comment",
|
|
686
|
+
"--issue",
|
|
687
|
+
linear_issue,
|
|
688
|
+
"--body",
|
|
689
|
+
comment_body,
|
|
690
|
+
],
|
|
691
|
+
capture_output=True,
|
|
692
|
+
text=True,
|
|
693
|
+
timeout=15,
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
if result.returncode == 0:
|
|
697
|
+
return True, f"Comment created on {linear_issue}"
|
|
698
|
+
else:
|
|
699
|
+
return False, f"Failed to create comment: {result.stderr}"
|
|
700
|
+
|
|
701
|
+
except subprocess.TimeoutExpired:
|
|
702
|
+
return False, "Linear API timed out"
|
|
703
|
+
except subprocess.SubprocessError as e:
|
|
704
|
+
return False, f"Linear API error: {e}"
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
# =============================================================================
|
|
708
|
+
# CLI Interface
|
|
709
|
+
# =============================================================================
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def main():
|
|
713
|
+
"""CLI entry point for testing."""
|
|
714
|
+
import argparse
|
|
715
|
+
|
|
716
|
+
parser = argparse.ArgumentParser(description="Generate CCS handoff document")
|
|
717
|
+
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
718
|
+
|
|
719
|
+
# Handoff command (default behavior)
|
|
720
|
+
handoff_parser = subparsers.add_parser("handoff", help="Generate handoff document")
|
|
721
|
+
handoff_parser.add_argument("--level", default="L2", help="Checkpoint level (L1, L2, L3)")
|
|
722
|
+
handoff_parser.add_argument("--percent", type=int, default=85, help="Context percentage")
|
|
723
|
+
handoff_parser.add_argument("--issue", default="", help="Linear issue ID")
|
|
724
|
+
handoff_parser.add_argument("--summary", default="", help="Session summary")
|
|
725
|
+
handoff_parser.add_argument("--save", action="store_true", help="Save to file")
|
|
726
|
+
handoff_parser.add_argument("--project", default=".", help="Project path")
|
|
727
|
+
|
|
728
|
+
# WIP commit command
|
|
729
|
+
wip_parser = subparsers.add_parser("wip", help="Create WIP checkpoint commit")
|
|
730
|
+
wip_parser.add_argument("--level", default="L2", help="Checkpoint level (L1, L2, L3)")
|
|
731
|
+
wip_parser.add_argument("--percent", type=int, default=85, help="Context percentage")
|
|
732
|
+
wip_parser.add_argument("--issue", default="", help="Linear issue ID")
|
|
733
|
+
wip_parser.add_argument("--project", default=".", help="Project path")
|
|
734
|
+
|
|
735
|
+
# Legacy support: if no command, treat as handoff
|
|
736
|
+
parser.add_argument("--level", default="L2", help="Checkpoint level (L1, L2, L3)")
|
|
737
|
+
parser.add_argument("--percent", type=int, default=85, help="Context percentage")
|
|
738
|
+
parser.add_argument("--issue", default="", help="Linear issue ID")
|
|
739
|
+
parser.add_argument("--summary", default="", help="Session summary")
|
|
740
|
+
parser.add_argument("--save", action="store_true", help="Save to file")
|
|
741
|
+
parser.add_argument("--project", default=".", help="Project path")
|
|
742
|
+
|
|
743
|
+
args = parser.parse_args()
|
|
744
|
+
|
|
745
|
+
generator = HandoffGenerator(project_path=args.project)
|
|
746
|
+
|
|
747
|
+
if args.command == "wip":
|
|
748
|
+
success, message = generator.create_wip_commit(
|
|
749
|
+
trigger_level=args.level,
|
|
750
|
+
context_percent=args.percent,
|
|
751
|
+
linear_issue=args.issue,
|
|
752
|
+
)
|
|
753
|
+
if success:
|
|
754
|
+
print(f"WIP commit created: {message}")
|
|
755
|
+
else:
|
|
756
|
+
print(f"No commit created: {message}")
|
|
757
|
+
exit(1)
|
|
758
|
+
else:
|
|
759
|
+
# Default: generate handoff document
|
|
760
|
+
doc = generator.generate(
|
|
761
|
+
trigger_level=args.level,
|
|
762
|
+
context_percent=args.percent,
|
|
763
|
+
linear_issue=args.issue,
|
|
764
|
+
summary=args.summary,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
if args.save:
|
|
768
|
+
filepath = generator.save(doc)
|
|
769
|
+
print(f"Saved to: {filepath}")
|
|
770
|
+
else:
|
|
771
|
+
print(generator.render_markdown(doc))
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
if __name__ == "__main__":
|
|
775
|
+
main()
|