aiwcli 0.9.0
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 +1248 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +16 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +19 -0
- package/dist/commands/branch.d.ts +45 -0
- package/dist/commands/branch.js +488 -0
- package/dist/commands/clean.d.ts +34 -0
- package/dist/commands/clean.js +186 -0
- package/dist/commands/clear.d.ts +51 -0
- package/dist/commands/clear.js +835 -0
- package/dist/commands/init/index.d.ts +107 -0
- package/dist/commands/init/index.js +565 -0
- package/dist/commands/launch.d.ts +21 -0
- package/dist/commands/launch.js +108 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/base-command.d.ts +114 -0
- package/dist/lib/base-command.js +153 -0
- package/dist/lib/bmad-installer.d.ts +38 -0
- package/dist/lib/bmad-installer.js +145 -0
- package/dist/lib/claude-settings-types.d.ts +102 -0
- package/dist/lib/claude-settings-types.js +5 -0
- package/dist/lib/config.d.ts +25 -0
- package/dist/lib/config.js +46 -0
- package/dist/lib/debug.d.ts +39 -0
- package/dist/lib/debug.js +74 -0
- package/dist/lib/env-compat.d.ts +26 -0
- package/dist/lib/env-compat.js +35 -0
- package/dist/lib/errors.d.ts +126 -0
- package/dist/lib/errors.js +145 -0
- package/dist/lib/generic-merge.d.ts +74 -0
- package/dist/lib/generic-merge.js +105 -0
- package/dist/lib/git/branch.d.ts +67 -0
- package/dist/lib/git/branch.js +155 -0
- package/dist/lib/git/index.d.ts +11 -0
- package/dist/lib/git/index.js +13 -0
- package/dist/lib/git/safety-checks.d.ts +44 -0
- package/dist/lib/git/safety-checks.js +102 -0
- package/dist/lib/git/types.d.ts +31 -0
- package/dist/lib/git/types.js +6 -0
- package/dist/lib/git/worktree.d.ts +67 -0
- package/dist/lib/git/worktree.js +220 -0
- package/dist/lib/gitignore-manager.d.ts +10 -0
- package/dist/lib/gitignore-manager.js +60 -0
- package/dist/lib/hooks-merger.d.ts +28 -0
- package/dist/lib/hooks-merger.js +94 -0
- package/dist/lib/ide-path-resolver.d.ts +102 -0
- package/dist/lib/ide-path-resolver.js +129 -0
- package/dist/lib/index.d.ts +13 -0
- package/dist/lib/index.js +22 -0
- package/dist/lib/output.d.ts +51 -0
- package/dist/lib/output.js +76 -0
- package/dist/lib/paths.d.ts +66 -0
- package/dist/lib/paths.js +136 -0
- package/dist/lib/quiet.d.ts +12 -0
- package/dist/lib/quiet.js +17 -0
- package/dist/lib/settings-hierarchy.d.ts +42 -0
- package/dist/lib/settings-hierarchy.js +105 -0
- package/dist/lib/spawn.d.ts +105 -0
- package/dist/lib/spawn.js +157 -0
- package/dist/lib/spinner.d.ts +19 -0
- package/dist/lib/spinner.js +34 -0
- package/dist/lib/stdin.d.ts +48 -0
- package/dist/lib/stdin.js +60 -0
- package/dist/lib/template-installer.d.ts +92 -0
- package/dist/lib/template-installer.js +375 -0
- package/dist/lib/template-linter.d.ts +49 -0
- package/dist/lib/template-linter.js +173 -0
- package/dist/lib/template-merger.d.ts +47 -0
- package/dist/lib/template-merger.js +173 -0
- package/dist/lib/template-resolver.d.ts +20 -0
- package/dist/lib/template-resolver.js +60 -0
- package/dist/lib/terminal.d.ts +102 -0
- package/dist/lib/terminal.js +245 -0
- package/dist/lib/tty-detection.d.ts +62 -0
- package/dist/lib/tty-detection.js +83 -0
- package/dist/lib/user-utils.d.ts +5 -0
- package/dist/lib/user-utils.js +23 -0
- package/dist/lib/version.d.ts +99 -0
- package/dist/lib/version.js +144 -0
- package/dist/lib/watch-templates.d.ts +6 -0
- package/dist/lib/watch-templates.js +73 -0
- package/dist/lib/windsurf-hooks-hierarchy.d.ts +30 -0
- package/dist/lib/windsurf-hooks-hierarchy.js +66 -0
- package/dist/lib/windsurf-hooks-merger.d.ts +26 -0
- package/dist/lib/windsurf-hooks-merger.js +53 -0
- package/dist/lib/windsurf-hooks-types.d.ts +33 -0
- package/dist/lib/windsurf-hooks-types.js +5 -0
- package/dist/templates/CLAUDE.md +174 -0
- package/dist/templates/_shared/.claude/commands/handoff.md +14 -0
- package/dist/templates/_shared/.claude/settings.json +61 -0
- package/dist/templates/_shared/.codex/workflows/handoff.md +14 -0
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +14 -0
- package/dist/templates/_shared/hooks/__init__.py +16 -0
- package/dist/templates/_shared/hooks/archive_plan.py +270 -0
- package/dist/templates/_shared/hooks/context_enforcer.py +621 -0
- package/dist/templates/_shared/hooks/context_monitor.py +322 -0
- package/dist/templates/_shared/hooks/file-suggestion.py +188 -0
- package/dist/templates/_shared/hooks/task_create_capture.py +194 -0
- package/dist/templates/_shared/hooks/task_update_capture.py +254 -0
- package/dist/templates/_shared/hooks/user_prompt_submit.py +157 -0
- package/dist/templates/_shared/lib/__init__.py +1 -0
- package/dist/templates/_shared/lib/base/__init__.py +49 -0
- package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/atomic_write.py +180 -0
- package/dist/templates/_shared/lib/base/constants.py +299 -0
- package/dist/templates/_shared/lib/base/inference.py +189 -0
- package/dist/templates/_shared/lib/base/utils.py +216 -0
- package/dist/templates/_shared/lib/context/__init__.py +119 -0
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/cache.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__/event_log.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/cache.py +446 -0
- package/dist/templates/_shared/lib/context/context_manager.py +1171 -0
- package/dist/templates/_shared/lib/context/discovery.py +486 -0
- package/dist/templates/_shared/lib/context/event_log.py +308 -0
- package/dist/templates/_shared/lib/context/plan_archive.py +247 -0
- package/dist/templates/_shared/lib/context/task_sync.py +367 -0
- package/dist/templates/_shared/lib/handoff/__init__.py +22 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +307 -0
- package/dist/templates/_shared/lib/templates/README.md +215 -0
- package/dist/templates/_shared/lib/templates/__init__.py +40 -0
- package/dist/templates/_shared/lib/templates/formatters.py +147 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +119 -0
- package/dist/templates/_shared/scripts/save_handoff.py +99 -0
- package/dist/templates/_shared/workflows/handoff.md +212 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/ACCESSIBILITY-TESTER.md +80 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/ARCHITECT-REVIEWER.md +75 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/ASSUMPTION-CHAIN-TRACER.md +239 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/CLARITY-AUDITOR.md +109 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/CODE-REVIEWER.md +71 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/COMPLETENESS-CHECKER.md +104 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/CONTEXT-EXTRACTOR.md +93 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/DEVILS-ADVOCATE.md +223 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/DOCUMENTATION-REVIEWER.md +73 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/FEASIBILITY-ANALYST.md +93 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/FRESH-PERSPECTIVE.md +103 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/HANDOFF-READINESS.md +145 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/HIDDEN-COMPLEXITY-DETECTOR.md +248 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/INCENTIVE-MAPPER.md +235 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PENETRATION-TESTER.md +80 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PERFORMANCE-ENGINEER.md +76 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PLAN-ORCHESTRATOR.md +141 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PRECEDENT-FINDER.md +240 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/REVERSIBILITY-ANALYST.md +211 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/RISK-ASSESSOR.md +101 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/SECOND-ORDER-ANALYST.md +197 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/SIMPLICITY-GUARDIAN.md +97 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/SKEPTIC.md +349 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/STAKEHOLDER-ADVOCATE.md +106 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/TRADE-OFF-ILLUMINATOR.md +205 -0
- package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +8 -0
- package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -0
- package/dist/templates/cc-native/.claude/settings.json +119 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +8 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -0
- package/dist/templates/cc-native/CC-NATIVE-README.md +192 -0
- package/dist/templates/cc-native/MIGRATION.md +86 -0
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +331 -0
- package/dist/templates/cc-native/_cc-native/docs/PERMISSION_REQUEST_VERIFICATION.md +147 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -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/__pycache__/test_permission_request.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +150 -0
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +746 -0
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +339 -0
- package/dist/templates/cc-native/_cc-native/lib/__init__.py +57 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/async_archive.py +68 -0
- package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +98 -0
- package/dist/templates/cc-native/_cc-native/lib/constants.py +45 -0
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +273 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +28 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -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/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +164 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +89 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +119 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +103 -0
- package/dist/templates/cc-native/_cc-native/lib/state.py +251 -0
- package/dist/templates/cc-native/_cc-native/lib/utils.py +830 -0
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +76 -0
- package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +151 -0
- package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +134 -0
- package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -0
- package/dist/types/exit-codes.d.ts +11 -0
- package/dist/types/exit-codes.js +10 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +7 -0
- package/oclif.manifest.json +405 -0
- package/package.json +109 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""Task synchronization utilities for Claude native task integration.
|
|
2
|
+
|
|
3
|
+
Provides bi-directional sync between:
|
|
4
|
+
- Claude Code native TaskCreate/TaskUpdate/TaskList tools (ephemeral)
|
|
5
|
+
- Persistent events.jsonl storage (source of truth)
|
|
6
|
+
|
|
7
|
+
SESSION START (Hydrate):
|
|
8
|
+
1. Read events.jsonl -> compute pending tasks
|
|
9
|
+
2. Output instructions for Claude to recreate tasks via TaskCreate
|
|
10
|
+
3. Claude's native TaskList now populated with persistent state
|
|
11
|
+
|
|
12
|
+
DURING SESSION (Persist):
|
|
13
|
+
1. Claude uses native TaskCreate/TaskUpdate
|
|
14
|
+
2. CLAUDE.md instructs: after TaskUpdate, call append_event()
|
|
15
|
+
3. Both systems stay in sync
|
|
16
|
+
|
|
17
|
+
SESSION END:
|
|
18
|
+
- events.jsonl already has everything
|
|
19
|
+
- Next session will hydrate from it
|
|
20
|
+
"""
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import List, Optional
|
|
23
|
+
|
|
24
|
+
from .event_log import (
|
|
25
|
+
get_current_state,
|
|
26
|
+
get_pending_tasks,
|
|
27
|
+
append_event,
|
|
28
|
+
Task,
|
|
29
|
+
EVENT_TASK_ADDED,
|
|
30
|
+
EVENT_TASK_STARTED,
|
|
31
|
+
EVENT_TASK_COMPLETED,
|
|
32
|
+
EVENT_TASK_BLOCKED,
|
|
33
|
+
EVENT_SESSION_STARTED,
|
|
34
|
+
)
|
|
35
|
+
from ..base.utils import eprint
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _escape_string(s: str) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Escape a string for safe embedding in quoted YAML-like format.
|
|
41
|
+
|
|
42
|
+
Handles backslashes, newlines, tabs, and quotes.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
s: Input string
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Escaped string safe for embedding in double quotes
|
|
49
|
+
"""
|
|
50
|
+
if not s:
|
|
51
|
+
return ""
|
|
52
|
+
# Order matters: escape backslashes first, then other special chars
|
|
53
|
+
s = s.replace('\\', '\\\\')
|
|
54
|
+
s = s.replace('\n', '\\n')
|
|
55
|
+
s = s.replace('\r', '\\r')
|
|
56
|
+
s = s.replace('\t', '\\t')
|
|
57
|
+
s = s.replace('"', '\\"')
|
|
58
|
+
return s
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def generate_hydration_instructions(
|
|
62
|
+
context_id: str,
|
|
63
|
+
project_root: Path = None
|
|
64
|
+
) -> str:
|
|
65
|
+
"""
|
|
66
|
+
Generate instructions for Claude to recreate tasks from persistent storage.
|
|
67
|
+
|
|
68
|
+
Called by SessionStart hook when resuming a context.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
context_id: Context identifier
|
|
72
|
+
project_root: Project root directory
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Formatted instructions for Claude to restore tasks
|
|
76
|
+
"""
|
|
77
|
+
pending_tasks = get_pending_tasks(context_id, project_root)
|
|
78
|
+
|
|
79
|
+
if not pending_tasks:
|
|
80
|
+
return "No pending tasks to restore."
|
|
81
|
+
|
|
82
|
+
lines = [
|
|
83
|
+
"## Restoring Tasks from Previous Session",
|
|
84
|
+
"",
|
|
85
|
+
"Please recreate these tasks using TaskCreate:",
|
|
86
|
+
"",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
for task in pending_tasks:
|
|
90
|
+
lines.append(f"### Task: {task.subject}")
|
|
91
|
+
lines.append("")
|
|
92
|
+
lines.append("```")
|
|
93
|
+
lines.append("TaskCreate:")
|
|
94
|
+
# Escape special characters for YAML-like format
|
|
95
|
+
subject_escaped = _escape_string(task.subject)
|
|
96
|
+
lines.append(f' subject: "{subject_escaped}"')
|
|
97
|
+
if task.description:
|
|
98
|
+
desc_escaped = _escape_string(task.description)
|
|
99
|
+
lines.append(f' description: "{desc_escaped}"')
|
|
100
|
+
if task.active_form:
|
|
101
|
+
active_form_escaped = _escape_string(task.active_form)
|
|
102
|
+
lines.append(f' activeForm: "{active_form_escaped}"')
|
|
103
|
+
lines.append(f' metadata: {{"persistent_id": "{task.id}", "context": "{context_id}", "skip_persistence": true}}')
|
|
104
|
+
lines.append("```")
|
|
105
|
+
lines.append("")
|
|
106
|
+
|
|
107
|
+
return "\n".join(lines)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def generate_task_summary(context_id: str, project_root: Path = None) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Generate a summary of all tasks in a context.
|
|
113
|
+
|
|
114
|
+
Useful for status checks and completion suggestions.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
context_id: Context identifier
|
|
118
|
+
project_root: Project root directory
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Formatted task summary
|
|
122
|
+
"""
|
|
123
|
+
state = get_current_state(context_id, project_root)
|
|
124
|
+
|
|
125
|
+
if not state.tasks:
|
|
126
|
+
return "No tasks in this context."
|
|
127
|
+
|
|
128
|
+
completed = [t for t in state.tasks if t.status == "completed"]
|
|
129
|
+
pending = [t for t in state.tasks if t.status == "pending"]
|
|
130
|
+
in_progress = [t for t in state.tasks if t.status == "in_progress"]
|
|
131
|
+
blocked = [t for t in state.tasks if t.status == "blocked"]
|
|
132
|
+
|
|
133
|
+
lines = [
|
|
134
|
+
f"## Task Summary for: {context_id}",
|
|
135
|
+
"",
|
|
136
|
+
f"**Total:** {len(state.tasks)} tasks",
|
|
137
|
+
f"**Completed:** {len(completed)} | **In Progress:** {len(in_progress)} | **Pending:** {len(pending)} | **Blocked:** {len(blocked)}",
|
|
138
|
+
"",
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
if completed:
|
|
142
|
+
lines.append("### Completed")
|
|
143
|
+
for t in completed:
|
|
144
|
+
lines.append(f"- [x] {t.subject}")
|
|
145
|
+
lines.append("")
|
|
146
|
+
|
|
147
|
+
if in_progress:
|
|
148
|
+
lines.append("### In Progress")
|
|
149
|
+
for t in in_progress:
|
|
150
|
+
lines.append(f"- [~] {t.subject}")
|
|
151
|
+
lines.append("")
|
|
152
|
+
|
|
153
|
+
if pending:
|
|
154
|
+
lines.append("### Pending")
|
|
155
|
+
for t in pending:
|
|
156
|
+
lines.append(f"- [ ] {t.subject}")
|
|
157
|
+
lines.append("")
|
|
158
|
+
|
|
159
|
+
if blocked:
|
|
160
|
+
lines.append("### Blocked")
|
|
161
|
+
for t in blocked:
|
|
162
|
+
lines.append(f"- [!] {t.subject}: {t.blocked_reason}")
|
|
163
|
+
lines.append("")
|
|
164
|
+
|
|
165
|
+
return "\n".join(lines)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def record_session_start(
|
|
169
|
+
context_id: str,
|
|
170
|
+
tasks_hydrated: Optional[List[str]] = None,
|
|
171
|
+
project_root: Path = None
|
|
172
|
+
) -> bool:
|
|
173
|
+
"""
|
|
174
|
+
Record a session_started event in the context's event log.
|
|
175
|
+
|
|
176
|
+
Called after SessionStart hook loads a context.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
context_id: Context identifier
|
|
180
|
+
tasks_hydrated: List of task IDs that were restored
|
|
181
|
+
project_root: Project root directory
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
True if event was recorded successfully
|
|
185
|
+
"""
|
|
186
|
+
event_data = {}
|
|
187
|
+
if tasks_hydrated:
|
|
188
|
+
event_data["tasks_hydrated"] = tasks_hydrated
|
|
189
|
+
|
|
190
|
+
return append_event(
|
|
191
|
+
context_id,
|
|
192
|
+
EVENT_SESSION_STARTED,
|
|
193
|
+
project_root,
|
|
194
|
+
**event_data
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def record_task_created(
|
|
199
|
+
context_id: str,
|
|
200
|
+
task_id: str,
|
|
201
|
+
subject: str,
|
|
202
|
+
description: str = "",
|
|
203
|
+
active_form: str = "",
|
|
204
|
+
project_root: Path = None
|
|
205
|
+
) -> bool:
|
|
206
|
+
"""
|
|
207
|
+
Record a task_added event in the context's event log.
|
|
208
|
+
|
|
209
|
+
Called when Claude creates a new task via TaskCreate.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
context_id: Context identifier
|
|
213
|
+
task_id: Persistent task ID (e.g., "aiw-1")
|
|
214
|
+
subject: Task subject (required)
|
|
215
|
+
description: Task description (optional)
|
|
216
|
+
active_form: Spinner text for in_progress status (optional)
|
|
217
|
+
project_root: Project root directory
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
True if event was recorded successfully
|
|
221
|
+
"""
|
|
222
|
+
event_data = {
|
|
223
|
+
"task_id": task_id,
|
|
224
|
+
"subject": subject,
|
|
225
|
+
}
|
|
226
|
+
if description:
|
|
227
|
+
event_data["description"] = description
|
|
228
|
+
if active_form:
|
|
229
|
+
event_data["activeForm"] = active_form
|
|
230
|
+
|
|
231
|
+
return append_event(
|
|
232
|
+
context_id,
|
|
233
|
+
EVENT_TASK_ADDED,
|
|
234
|
+
project_root,
|
|
235
|
+
**event_data
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def record_task_started(
|
|
240
|
+
context_id: str,
|
|
241
|
+
task_id: str,
|
|
242
|
+
project_root: Path = None
|
|
243
|
+
) -> bool:
|
|
244
|
+
"""
|
|
245
|
+
Record a task_started event in the context's event log.
|
|
246
|
+
|
|
247
|
+
Called when Claude starts working on a task.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
context_id: Context identifier
|
|
251
|
+
task_id: Persistent task ID
|
|
252
|
+
project_root: Project root directory
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
True if event was recorded successfully
|
|
256
|
+
"""
|
|
257
|
+
return append_event(
|
|
258
|
+
context_id,
|
|
259
|
+
EVENT_TASK_STARTED,
|
|
260
|
+
project_root,
|
|
261
|
+
task_id=task_id
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def record_task_completed(
|
|
266
|
+
context_id: str,
|
|
267
|
+
task_id: str,
|
|
268
|
+
evidence: str,
|
|
269
|
+
work_summary: str = "",
|
|
270
|
+
files_changed: Optional[List[str]] = None,
|
|
271
|
+
commit_ref: str = "",
|
|
272
|
+
project_root: Path = None
|
|
273
|
+
) -> bool:
|
|
274
|
+
"""
|
|
275
|
+
Record a task_completed event in the context's event log.
|
|
276
|
+
|
|
277
|
+
Called when Claude completes a task.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
context_id: Context identifier
|
|
281
|
+
task_id: Persistent task ID
|
|
282
|
+
evidence: Verification evidence (required)
|
|
283
|
+
work_summary: Summary of work done (optional)
|
|
284
|
+
files_changed: List of files modified (optional)
|
|
285
|
+
commit_ref: Git commit reference (optional)
|
|
286
|
+
project_root: Project root directory
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
True if event was recorded successfully
|
|
290
|
+
"""
|
|
291
|
+
event_data = {
|
|
292
|
+
"task_id": task_id,
|
|
293
|
+
"evidence": evidence,
|
|
294
|
+
}
|
|
295
|
+
if work_summary:
|
|
296
|
+
event_data["work_summary"] = work_summary
|
|
297
|
+
if files_changed:
|
|
298
|
+
event_data["files_changed"] = files_changed
|
|
299
|
+
if commit_ref:
|
|
300
|
+
event_data["commit_ref"] = commit_ref
|
|
301
|
+
|
|
302
|
+
return append_event(
|
|
303
|
+
context_id,
|
|
304
|
+
EVENT_TASK_COMPLETED,
|
|
305
|
+
project_root,
|
|
306
|
+
**event_data
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def record_task_blocked(
|
|
311
|
+
context_id: str,
|
|
312
|
+
task_id: str,
|
|
313
|
+
reason: str,
|
|
314
|
+
project_root: Path = None
|
|
315
|
+
) -> bool:
|
|
316
|
+
"""
|
|
317
|
+
Record a task_blocked event in the context's event log.
|
|
318
|
+
|
|
319
|
+
Called when a task becomes blocked.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
context_id: Context identifier
|
|
323
|
+
task_id: Persistent task ID
|
|
324
|
+
reason: Reason for being blocked
|
|
325
|
+
project_root: Project root directory
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
True if event was recorded successfully
|
|
329
|
+
"""
|
|
330
|
+
return append_event(
|
|
331
|
+
context_id,
|
|
332
|
+
EVENT_TASK_BLOCKED,
|
|
333
|
+
project_root,
|
|
334
|
+
task_id=task_id,
|
|
335
|
+
reason=reason
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def generate_next_task_id(context_id: str, project_root: Path = None) -> str:
|
|
340
|
+
"""
|
|
341
|
+
Generate the next sequential task ID for a context.
|
|
342
|
+
|
|
343
|
+
Task IDs follow the pattern: aiw-{n} where n starts at 1.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
context_id: Context identifier
|
|
347
|
+
project_root: Project root directory
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Next available task ID (e.g., "aiw-3")
|
|
351
|
+
"""
|
|
352
|
+
state = get_current_state(context_id, project_root)
|
|
353
|
+
|
|
354
|
+
if not state.tasks:
|
|
355
|
+
return "aiw-1"
|
|
356
|
+
|
|
357
|
+
# Find highest existing task number
|
|
358
|
+
max_num = 0
|
|
359
|
+
for task in state.tasks:
|
|
360
|
+
if task.id.startswith("aiw-"):
|
|
361
|
+
try:
|
|
362
|
+
num = int(task.id.split("-")[1])
|
|
363
|
+
max_num = max(max_num, num)
|
|
364
|
+
except (IndexError, ValueError):
|
|
365
|
+
pass
|
|
366
|
+
|
|
367
|
+
return f"aiw-{max_num + 1}"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Handoff utilities for context-aware session management.
|
|
2
|
+
|
|
3
|
+
This module provides graceful context degradation when Claude's
|
|
4
|
+
context window fills up. Instead of rushing or losing work,
|
|
5
|
+
it creates a handoff document and facilitates clean session continuation.
|
|
6
|
+
|
|
7
|
+
Components:
|
|
8
|
+
- document_generator: Creates handoff documents with work state
|
|
9
|
+
- context_monitor hook: Monitors context during tool use and triggers warnings
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .document_generator import (
|
|
13
|
+
generate_handoff_document,
|
|
14
|
+
get_handoff_continuation_prompt,
|
|
15
|
+
HandoffDocument,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"generate_handoff_document",
|
|
20
|
+
"get_handoff_continuation_prompt",
|
|
21
|
+
"HandoffDocument",
|
|
22
|
+
]
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Handoff document generator for context-aware session management.
|
|
2
|
+
|
|
3
|
+
Creates structured handoff documents when a session needs to transfer
|
|
4
|
+
work to a new session (typically due to context window limits).
|
|
5
|
+
|
|
6
|
+
Handoff documents capture:
|
|
7
|
+
- Links to active plan and context folder
|
|
8
|
+
- Current task state from events.jsonl
|
|
9
|
+
- Work in progress summary
|
|
10
|
+
- Next steps for continuation
|
|
11
|
+
"""
|
|
12
|
+
import json
|
|
13
|
+
import uuid
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
from ..base.atomic_write import atomic_write
|
|
20
|
+
from ..base.constants import get_context_handoffs_dir, get_context_dir
|
|
21
|
+
from ..base.utils import eprint, now_iso
|
|
22
|
+
from ..context.event_log import (
|
|
23
|
+
append_event,
|
|
24
|
+
get_current_state,
|
|
25
|
+
get_pending_tasks,
|
|
26
|
+
Task,
|
|
27
|
+
EVENT_HANDOFF_CREATED,
|
|
28
|
+
)
|
|
29
|
+
from ..templates.formatters import render_task_list, format_continuation_header, format_reason
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class HandoffDocument:
|
|
34
|
+
"""Structured handoff document content."""
|
|
35
|
+
context_id: str
|
|
36
|
+
context_summary: str
|
|
37
|
+
session_id: str
|
|
38
|
+
reason: str # e.g., "low_context", "user_requested", "error_recovery"
|
|
39
|
+
created_at: str
|
|
40
|
+
|
|
41
|
+
# Links
|
|
42
|
+
plan_path: Optional[str] = None
|
|
43
|
+
context_folder: str = ""
|
|
44
|
+
events_log_path: str = ""
|
|
45
|
+
|
|
46
|
+
# Task state
|
|
47
|
+
active_tasks: List[Dict[str, Any]] = field(default_factory=list)
|
|
48
|
+
completed_tasks_this_session: List[Dict[str, Any]] = field(default_factory=list)
|
|
49
|
+
|
|
50
|
+
# Context summary
|
|
51
|
+
work_summary: str = ""
|
|
52
|
+
next_steps: List[str] = field(default_factory=list)
|
|
53
|
+
important_notes: List[str] = field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
# File path (set after saving)
|
|
56
|
+
file_path: Optional[str] = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def generate_handoff_document(
|
|
60
|
+
context_id: str,
|
|
61
|
+
reason: str = "low_context",
|
|
62
|
+
work_summary: str = "",
|
|
63
|
+
next_steps: Optional[List[str]] = None,
|
|
64
|
+
important_notes: Optional[List[str]] = None,
|
|
65
|
+
completed_this_session: Optional[List[str]] = None,
|
|
66
|
+
project_root: Path = None
|
|
67
|
+
) -> Optional[HandoffDocument]:
|
|
68
|
+
"""
|
|
69
|
+
Generate and save a handoff document for a context.
|
|
70
|
+
|
|
71
|
+
This creates a markdown document capturing current work state,
|
|
72
|
+
saves it to the context's handoffs folder, and records the event.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
context_id: Context identifier
|
|
76
|
+
reason: Why handoff is happening (low_context, user_requested, etc.)
|
|
77
|
+
work_summary: Summary of current work in progress
|
|
78
|
+
next_steps: List of next steps for continuation
|
|
79
|
+
important_notes: Important decisions or context to preserve
|
|
80
|
+
completed_this_session: List of task subjects completed this session
|
|
81
|
+
project_root: Project root directory
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
HandoffDocument with file_path set, or None on failure
|
|
85
|
+
"""
|
|
86
|
+
from ..context.context_manager import get_context, update_handoff_status
|
|
87
|
+
|
|
88
|
+
context = get_context(context_id, project_root)
|
|
89
|
+
if not context:
|
|
90
|
+
eprint(f"[handoff] ERROR: Context '{context_id}' not found")
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
# Generate session ID
|
|
94
|
+
session_id = str(uuid.uuid4())[:8]
|
|
95
|
+
|
|
96
|
+
# Get current state
|
|
97
|
+
state = get_current_state(context_id, project_root)
|
|
98
|
+
pending_tasks = get_pending_tasks(context_id, project_root)
|
|
99
|
+
|
|
100
|
+
# Build document
|
|
101
|
+
now = now_iso()
|
|
102
|
+
context_dir = get_context_dir(context_id, project_root)
|
|
103
|
+
|
|
104
|
+
doc = HandoffDocument(
|
|
105
|
+
context_id=context_id,
|
|
106
|
+
context_summary=context.summary,
|
|
107
|
+
session_id=session_id,
|
|
108
|
+
reason=reason,
|
|
109
|
+
created_at=now,
|
|
110
|
+
plan_path=context.in_flight.artifact_path if context.in_flight else None,
|
|
111
|
+
context_folder=str(context_dir),
|
|
112
|
+
events_log_path=str(context_dir / "events.jsonl"),
|
|
113
|
+
active_tasks=[_task_to_dict(t) for t in pending_tasks],
|
|
114
|
+
completed_tasks_this_session=[
|
|
115
|
+
{"subject": s} for s in (completed_this_session or [])
|
|
116
|
+
],
|
|
117
|
+
work_summary=work_summary,
|
|
118
|
+
next_steps=next_steps or [],
|
|
119
|
+
important_notes=important_notes or [],
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Compute file path BEFORE rendering markdown
|
|
123
|
+
handoffs_dir = get_context_handoffs_dir(context_id, project_root)
|
|
124
|
+
handoffs_dir.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
|
|
126
|
+
# Filename: YYYY-MM-DD-session-{session_id}.md
|
|
127
|
+
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
128
|
+
filename = f"{date_str}-session-{session_id}.md"
|
|
129
|
+
file_path = handoffs_dir / filename
|
|
130
|
+
|
|
131
|
+
# Set file_path on doc BEFORE rendering markdown
|
|
132
|
+
doc.file_path = str(file_path)
|
|
133
|
+
|
|
134
|
+
# Generate markdown content
|
|
135
|
+
markdown = _render_handoff_markdown(doc)
|
|
136
|
+
|
|
137
|
+
# Save to handoffs folder
|
|
138
|
+
|
|
139
|
+
success, error = atomic_write(file_path, markdown)
|
|
140
|
+
if not success:
|
|
141
|
+
eprint(f"[handoff] ERROR: Failed to write handoff document: {error}")
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
# Record event
|
|
145
|
+
append_event(
|
|
146
|
+
context_id,
|
|
147
|
+
EVENT_HANDOFF_CREATED,
|
|
148
|
+
project_root,
|
|
149
|
+
path=str(file_path),
|
|
150
|
+
reason=reason,
|
|
151
|
+
session_id=session_id
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Update context in_flight state
|
|
155
|
+
update_handoff_status(context_id, str(file_path), project_root)
|
|
156
|
+
|
|
157
|
+
eprint(f"[handoff] Created handoff document: {file_path}")
|
|
158
|
+
return doc
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _task_to_dict(task: Task) -> Dict[str, Any]:
|
|
162
|
+
"""Convert Task to dictionary for handoff document."""
|
|
163
|
+
return {
|
|
164
|
+
"id": task.id,
|
|
165
|
+
"subject": task.subject,
|
|
166
|
+
"status": task.status,
|
|
167
|
+
"description": task.description,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _render_handoff_markdown(doc: HandoffDocument) -> str:
|
|
172
|
+
"""Render handoff document as markdown."""
|
|
173
|
+
lines = [
|
|
174
|
+
format_continuation_header("handoff", doc.context_id),
|
|
175
|
+
"",
|
|
176
|
+
f"**Created**: {doc.created_at}",
|
|
177
|
+
f"**Context ID**: {doc.context_id}",
|
|
178
|
+
f"**Session ID**: {doc.session_id}",
|
|
179
|
+
f"**Reason**: {format_reason(doc.reason)}",
|
|
180
|
+
"",
|
|
181
|
+
"## Links",
|
|
182
|
+
"",
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
# Plan link
|
|
186
|
+
if doc.plan_path:
|
|
187
|
+
lines.append(f"- **Plan**: [{Path(doc.plan_path).name}]({doc.plan_path})")
|
|
188
|
+
|
|
189
|
+
lines.extend([
|
|
190
|
+
f"- **Context Folder**: `{doc.context_folder}`",
|
|
191
|
+
f"- **Events Log**: `{doc.events_log_path}`",
|
|
192
|
+
"",
|
|
193
|
+
"## Current State",
|
|
194
|
+
"",
|
|
195
|
+
])
|
|
196
|
+
|
|
197
|
+
# Active tasks
|
|
198
|
+
lines.append(render_task_list(doc.active_tasks, header="Active Tasks", show_description=True).rstrip())
|
|
199
|
+
lines.append("")
|
|
200
|
+
|
|
201
|
+
# Completed this session
|
|
202
|
+
if doc.completed_tasks_this_session:
|
|
203
|
+
lines.append(render_task_list(
|
|
204
|
+
doc.completed_tasks_this_session,
|
|
205
|
+
header="Completed This Session",
|
|
206
|
+
show_description=False
|
|
207
|
+
).rstrip())
|
|
208
|
+
lines.append("")
|
|
209
|
+
|
|
210
|
+
# Work summary
|
|
211
|
+
if doc.work_summary:
|
|
212
|
+
lines.extend([
|
|
213
|
+
"## Context Summary",
|
|
214
|
+
"",
|
|
215
|
+
doc.work_summary,
|
|
216
|
+
"",
|
|
217
|
+
])
|
|
218
|
+
|
|
219
|
+
# Next steps
|
|
220
|
+
if doc.next_steps:
|
|
221
|
+
lines.extend([
|
|
222
|
+
"## Next Steps",
|
|
223
|
+
"",
|
|
224
|
+
])
|
|
225
|
+
for i, step in enumerate(doc.next_steps, 1):
|
|
226
|
+
lines.append(f"{i}. {step}")
|
|
227
|
+
lines.append("")
|
|
228
|
+
|
|
229
|
+
# Important notes
|
|
230
|
+
if doc.important_notes:
|
|
231
|
+
lines.extend([
|
|
232
|
+
"## Important Notes",
|
|
233
|
+
"",
|
|
234
|
+
])
|
|
235
|
+
for note in doc.important_notes:
|
|
236
|
+
lines.append(f"- {note}")
|
|
237
|
+
lines.append("")
|
|
238
|
+
|
|
239
|
+
# Continuation prompt
|
|
240
|
+
lines.extend([
|
|
241
|
+
"---",
|
|
242
|
+
"",
|
|
243
|
+
"**Continuation Prompt**:",
|
|
244
|
+
"```",
|
|
245
|
+
f'Continue working on context "{doc.context_id}".',
|
|
246
|
+
"",
|
|
247
|
+
f"Handoff document: {doc.file_path or 'See above'}",
|
|
248
|
+
"",
|
|
249
|
+
"Read the handoff document, restore tasks with TaskCreate, and continue implementation.",
|
|
250
|
+
"```",
|
|
251
|
+
])
|
|
252
|
+
|
|
253
|
+
return "\n".join(lines)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def get_handoff_continuation_prompt(doc: HandoffDocument) -> str:
|
|
257
|
+
"""
|
|
258
|
+
Generate the prompt to paste into new session for continuation.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
doc: HandoffDocument with file_path set
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Prompt string for continuing work
|
|
265
|
+
"""
|
|
266
|
+
return f"""Continue working on context "{doc.context_id}".
|
|
267
|
+
|
|
268
|
+
Handoff document: {doc.file_path}
|
|
269
|
+
|
|
270
|
+
Read the handoff document, restore tasks with TaskCreate, and continue implementation."""
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def get_low_context_warning(context_remaining_percent: int, context_id: str) -> str:
|
|
274
|
+
"""
|
|
275
|
+
Generate system reminder for low context warning.
|
|
276
|
+
|
|
277
|
+
This is injected by the UserPromptSubmit hook when context is low.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
context_remaining_percent: Percentage of context remaining
|
|
281
|
+
context_id: Current context identifier
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
System reminder markdown
|
|
285
|
+
"""
|
|
286
|
+
return f"""<system-reminder>
|
|
287
|
+
## LOW CONTEXT WARNING ({context_remaining_percent}% remaining)
|
|
288
|
+
|
|
289
|
+
Your context window is running low. Please:
|
|
290
|
+
|
|
291
|
+
1. **Finish current task** if 1-2 steps away, OR save current progress
|
|
292
|
+
2. **Create handoff document** by calling:
|
|
293
|
+
```python
|
|
294
|
+
from _shared.lib.handoff import generate_handoff_document
|
|
295
|
+
doc = generate_handoff_document(
|
|
296
|
+
context_id="{context_id}",
|
|
297
|
+
reason="low_context",
|
|
298
|
+
work_summary="<describe current work>",
|
|
299
|
+
next_steps=["<step 1>", "<step 2>"],
|
|
300
|
+
important_notes=["<key decision 1>"]
|
|
301
|
+
)
|
|
302
|
+
```
|
|
303
|
+
3. **Ask permission** to clear and paste continuation prompt
|
|
304
|
+
|
|
305
|
+
After creating handoff, ask the user:
|
|
306
|
+
"Context is low. I've created a handoff document. May I clear and continue in a new session?"
|
|
307
|
+
</system-reminder>"""
|