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.
Files changed (204) hide show
  1. package/README.md +1248 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +16 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +19 -0
  6. package/dist/commands/branch.d.ts +45 -0
  7. package/dist/commands/branch.js +488 -0
  8. package/dist/commands/clean.d.ts +34 -0
  9. package/dist/commands/clean.js +186 -0
  10. package/dist/commands/clear.d.ts +51 -0
  11. package/dist/commands/clear.js +835 -0
  12. package/dist/commands/init/index.d.ts +107 -0
  13. package/dist/commands/init/index.js +565 -0
  14. package/dist/commands/launch.d.ts +21 -0
  15. package/dist/commands/launch.js +108 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/lib/base-command.d.ts +114 -0
  19. package/dist/lib/base-command.js +153 -0
  20. package/dist/lib/bmad-installer.d.ts +38 -0
  21. package/dist/lib/bmad-installer.js +145 -0
  22. package/dist/lib/claude-settings-types.d.ts +102 -0
  23. package/dist/lib/claude-settings-types.js +5 -0
  24. package/dist/lib/config.d.ts +25 -0
  25. package/dist/lib/config.js +46 -0
  26. package/dist/lib/debug.d.ts +39 -0
  27. package/dist/lib/debug.js +74 -0
  28. package/dist/lib/env-compat.d.ts +26 -0
  29. package/dist/lib/env-compat.js +35 -0
  30. package/dist/lib/errors.d.ts +126 -0
  31. package/dist/lib/errors.js +145 -0
  32. package/dist/lib/generic-merge.d.ts +74 -0
  33. package/dist/lib/generic-merge.js +105 -0
  34. package/dist/lib/git/branch.d.ts +67 -0
  35. package/dist/lib/git/branch.js +155 -0
  36. package/dist/lib/git/index.d.ts +11 -0
  37. package/dist/lib/git/index.js +13 -0
  38. package/dist/lib/git/safety-checks.d.ts +44 -0
  39. package/dist/lib/git/safety-checks.js +102 -0
  40. package/dist/lib/git/types.d.ts +31 -0
  41. package/dist/lib/git/types.js +6 -0
  42. package/dist/lib/git/worktree.d.ts +67 -0
  43. package/dist/lib/git/worktree.js +220 -0
  44. package/dist/lib/gitignore-manager.d.ts +10 -0
  45. package/dist/lib/gitignore-manager.js +60 -0
  46. package/dist/lib/hooks-merger.d.ts +28 -0
  47. package/dist/lib/hooks-merger.js +94 -0
  48. package/dist/lib/ide-path-resolver.d.ts +102 -0
  49. package/dist/lib/ide-path-resolver.js +129 -0
  50. package/dist/lib/index.d.ts +13 -0
  51. package/dist/lib/index.js +22 -0
  52. package/dist/lib/output.d.ts +51 -0
  53. package/dist/lib/output.js +76 -0
  54. package/dist/lib/paths.d.ts +66 -0
  55. package/dist/lib/paths.js +136 -0
  56. package/dist/lib/quiet.d.ts +12 -0
  57. package/dist/lib/quiet.js +17 -0
  58. package/dist/lib/settings-hierarchy.d.ts +42 -0
  59. package/dist/lib/settings-hierarchy.js +105 -0
  60. package/dist/lib/spawn.d.ts +105 -0
  61. package/dist/lib/spawn.js +157 -0
  62. package/dist/lib/spinner.d.ts +19 -0
  63. package/dist/lib/spinner.js +34 -0
  64. package/dist/lib/stdin.d.ts +48 -0
  65. package/dist/lib/stdin.js +60 -0
  66. package/dist/lib/template-installer.d.ts +92 -0
  67. package/dist/lib/template-installer.js +375 -0
  68. package/dist/lib/template-linter.d.ts +49 -0
  69. package/dist/lib/template-linter.js +173 -0
  70. package/dist/lib/template-merger.d.ts +47 -0
  71. package/dist/lib/template-merger.js +173 -0
  72. package/dist/lib/template-resolver.d.ts +20 -0
  73. package/dist/lib/template-resolver.js +60 -0
  74. package/dist/lib/terminal.d.ts +102 -0
  75. package/dist/lib/terminal.js +245 -0
  76. package/dist/lib/tty-detection.d.ts +62 -0
  77. package/dist/lib/tty-detection.js +83 -0
  78. package/dist/lib/user-utils.d.ts +5 -0
  79. package/dist/lib/user-utils.js +23 -0
  80. package/dist/lib/version.d.ts +99 -0
  81. package/dist/lib/version.js +144 -0
  82. package/dist/lib/watch-templates.d.ts +6 -0
  83. package/dist/lib/watch-templates.js +73 -0
  84. package/dist/lib/windsurf-hooks-hierarchy.d.ts +30 -0
  85. package/dist/lib/windsurf-hooks-hierarchy.js +66 -0
  86. package/dist/lib/windsurf-hooks-merger.d.ts +26 -0
  87. package/dist/lib/windsurf-hooks-merger.js +53 -0
  88. package/dist/lib/windsurf-hooks-types.d.ts +33 -0
  89. package/dist/lib/windsurf-hooks-types.js +5 -0
  90. package/dist/templates/CLAUDE.md +174 -0
  91. package/dist/templates/_shared/.claude/commands/handoff.md +14 -0
  92. package/dist/templates/_shared/.claude/settings.json +61 -0
  93. package/dist/templates/_shared/.codex/workflows/handoff.md +14 -0
  94. package/dist/templates/_shared/.windsurf/workflows/handoff.md +14 -0
  95. package/dist/templates/_shared/hooks/__init__.py +16 -0
  96. package/dist/templates/_shared/hooks/archive_plan.py +270 -0
  97. package/dist/templates/_shared/hooks/context_enforcer.py +621 -0
  98. package/dist/templates/_shared/hooks/context_monitor.py +322 -0
  99. package/dist/templates/_shared/hooks/file-suggestion.py +188 -0
  100. package/dist/templates/_shared/hooks/task_create_capture.py +194 -0
  101. package/dist/templates/_shared/hooks/task_update_capture.py +254 -0
  102. package/dist/templates/_shared/hooks/user_prompt_submit.py +157 -0
  103. package/dist/templates/_shared/lib/__init__.py +1 -0
  104. package/dist/templates/_shared/lib/base/__init__.py +49 -0
  105. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  106. package/dist/templates/_shared/lib/base/atomic_write.py +180 -0
  107. package/dist/templates/_shared/lib/base/constants.py +299 -0
  108. package/dist/templates/_shared/lib/base/inference.py +189 -0
  109. package/dist/templates/_shared/lib/base/utils.py +216 -0
  110. package/dist/templates/_shared/lib/context/__init__.py +119 -0
  111. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  112. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  113. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  114. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  115. package/dist/templates/_shared/lib/context/cache.py +446 -0
  116. package/dist/templates/_shared/lib/context/context_manager.py +1171 -0
  117. package/dist/templates/_shared/lib/context/discovery.py +486 -0
  118. package/dist/templates/_shared/lib/context/event_log.py +308 -0
  119. package/dist/templates/_shared/lib/context/plan_archive.py +247 -0
  120. package/dist/templates/_shared/lib/context/task_sync.py +367 -0
  121. package/dist/templates/_shared/lib/handoff/__init__.py +22 -0
  122. package/dist/templates/_shared/lib/handoff/document_generator.py +307 -0
  123. package/dist/templates/_shared/lib/templates/README.md +215 -0
  124. package/dist/templates/_shared/lib/templates/__init__.py +40 -0
  125. package/dist/templates/_shared/lib/templates/formatters.py +147 -0
  126. package/dist/templates/_shared/lib/templates/plan_context.py +119 -0
  127. package/dist/templates/_shared/scripts/save_handoff.py +99 -0
  128. package/dist/templates/_shared/workflows/handoff.md +212 -0
  129. package/dist/templates/cc-native/.claude/agents/cc-native/ACCESSIBILITY-TESTER.md +80 -0
  130. package/dist/templates/cc-native/.claude/agents/cc-native/ARCHITECT-REVIEWER.md +75 -0
  131. package/dist/templates/cc-native/.claude/agents/cc-native/ASSUMPTION-CHAIN-TRACER.md +239 -0
  132. package/dist/templates/cc-native/.claude/agents/cc-native/CLARITY-AUDITOR.md +109 -0
  133. package/dist/templates/cc-native/.claude/agents/cc-native/CODE-REVIEWER.md +71 -0
  134. package/dist/templates/cc-native/.claude/agents/cc-native/COMPLETENESS-CHECKER.md +104 -0
  135. package/dist/templates/cc-native/.claude/agents/cc-native/CONTEXT-EXTRACTOR.md +93 -0
  136. package/dist/templates/cc-native/.claude/agents/cc-native/DEVILS-ADVOCATE.md +223 -0
  137. package/dist/templates/cc-native/.claude/agents/cc-native/DOCUMENTATION-REVIEWER.md +73 -0
  138. package/dist/templates/cc-native/.claude/agents/cc-native/FEASIBILITY-ANALYST.md +93 -0
  139. package/dist/templates/cc-native/.claude/agents/cc-native/FRESH-PERSPECTIVE.md +103 -0
  140. package/dist/templates/cc-native/.claude/agents/cc-native/HANDOFF-READINESS.md +145 -0
  141. package/dist/templates/cc-native/.claude/agents/cc-native/HIDDEN-COMPLEXITY-DETECTOR.md +248 -0
  142. package/dist/templates/cc-native/.claude/agents/cc-native/INCENTIVE-MAPPER.md +235 -0
  143. package/dist/templates/cc-native/.claude/agents/cc-native/PENETRATION-TESTER.md +80 -0
  144. package/dist/templates/cc-native/.claude/agents/cc-native/PERFORMANCE-ENGINEER.md +76 -0
  145. package/dist/templates/cc-native/.claude/agents/cc-native/PLAN-ORCHESTRATOR.md +141 -0
  146. package/dist/templates/cc-native/.claude/agents/cc-native/PRECEDENT-FINDER.md +240 -0
  147. package/dist/templates/cc-native/.claude/agents/cc-native/REVERSIBILITY-ANALYST.md +211 -0
  148. package/dist/templates/cc-native/.claude/agents/cc-native/RISK-ASSESSOR.md +101 -0
  149. package/dist/templates/cc-native/.claude/agents/cc-native/SECOND-ORDER-ANALYST.md +197 -0
  150. package/dist/templates/cc-native/.claude/agents/cc-native/SIMPLICITY-GUARDIAN.md +97 -0
  151. package/dist/templates/cc-native/.claude/agents/cc-native/SKEPTIC.md +349 -0
  152. package/dist/templates/cc-native/.claude/agents/cc-native/STAKEHOLDER-ADVOCATE.md +106 -0
  153. package/dist/templates/cc-native/.claude/agents/cc-native/TRADE-OFF-ILLUMINATOR.md +205 -0
  154. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +8 -0
  155. package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -0
  156. package/dist/templates/cc-native/.claude/settings.json +119 -0
  157. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -0
  158. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +8 -0
  159. package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -0
  160. package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -0
  161. package/dist/templates/cc-native/CC-NATIVE-README.md +192 -0
  162. package/dist/templates/cc-native/MIGRATION.md +86 -0
  163. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +331 -0
  164. package/dist/templates/cc-native/_cc-native/docs/PERMISSION_REQUEST_VERIFICATION.md +147 -0
  165. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  166. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  167. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  168. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  169. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  170. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +150 -0
  171. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +746 -0
  172. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +339 -0
  173. package/dist/templates/cc-native/_cc-native/lib/__init__.py +57 -0
  174. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  175. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  176. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  177. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  178. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +68 -0
  179. package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +98 -0
  180. package/dist/templates/cc-native/_cc-native/lib/constants.py +45 -0
  181. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +273 -0
  182. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +28 -0
  183. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  184. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  185. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  186. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  187. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  188. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +164 -0
  189. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +89 -0
  190. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +119 -0
  191. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +103 -0
  192. package/dist/templates/cc-native/_cc-native/lib/state.py +251 -0
  193. package/dist/templates/cc-native/_cc-native/lib/utils.py +830 -0
  194. package/dist/templates/cc-native/_cc-native/plan-review.config.json +76 -0
  195. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  196. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +151 -0
  197. package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +134 -0
  198. package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -0
  199. package/dist/types/exit-codes.d.ts +11 -0
  200. package/dist/types/exit-codes.js +10 -0
  201. package/dist/types/index.d.ts +5 -0
  202. package/dist/types/index.js +7 -0
  203. package/oclif.manifest.json +405 -0
  204. package/package.json +109 -0
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env python3
2
+ """Context monitor hook for proactive handoff warnings.
3
+
4
+ This hook runs on PostToolUse for context-heavy tools and monitors
5
+ context window usage. When context drops below a threshold, it injects
6
+ a system reminder instructing Claude to wrap up and create a handoff document.
7
+
8
+ Unlike UserPromptSubmit hooks, this fires DURING Claude's work,
9
+ allowing proactive intervention without waiting for user input.
10
+
11
+ Monitored tools (configured via settings.json matcher):
12
+ - Task: Subagent responses can be huge
13
+ - Read: File content loads into context
14
+ - Bash: Command output can be large
15
+ - WebFetch: Web content loads into context
16
+
17
+ Hook input (from Claude Code):
18
+ {
19
+ "hook_event_name": "PostToolUse",
20
+ "tool_name": "Task",
21
+ "tool_input": {...},
22
+ "tool_result": {...},
23
+ "transcript_path": "/path/to/transcript.jsonl",
24
+ "session_id": "abc123",
25
+ "context_window": {
26
+ "current_usage": {
27
+ "cache_read_input_tokens": 0,
28
+ "input_tokens": 12345,
29
+ "cache_creation_input_tokens": 0,
30
+ "output_tokens": 6789
31
+ },
32
+ "context_window_size": 200000
33
+ },
34
+ ...
35
+ }
36
+
37
+ Hook output:
38
+ - Outputs JSON with additionalContext if context is low
39
+ - This injects a system reminder into Claude's context
40
+ - Plain stdout from PostToolUse only goes to verbose mode, not Claude
41
+ - Using additionalContext ensures Claude sees and responds to the warning
42
+
43
+ KNOWN LIMITATION: Context percentage won't match /context exactly.
44
+ Hook JSON excludes system prompt, tools, MCP tokens. We add a baseline
45
+ to compensate (~22.6k tokens typical). See:
46
+ https://github.com/anthropics/claude-code/issues/13783
47
+ """
48
+ import json
49
+ import sys
50
+ from pathlib import Path
51
+ from typing import Optional, Tuple
52
+
53
+ # Add parent directories to path for imports
54
+ SCRIPT_DIR = Path(__file__).resolve().parent
55
+ SHARED_LIB = SCRIPT_DIR.parent / "lib"
56
+ sys.path.insert(0, str(SHARED_LIB.parent))
57
+
58
+ from lib.base.utils import eprint, project_dir
59
+ from lib.context.context_manager import (
60
+ get_all_contexts,
61
+ get_context_by_session_id,
62
+ update_plan_status,
63
+ )
64
+
65
+ # Configuration
66
+ LOW_CONTEXT_THRESHOLD = 40 # Warn when below 40% remaining
67
+ CRITICAL_CONTEXT_THRESHOLD = 25 # Urgent warning below 25%
68
+
69
+ # Context baseline: preloaded tokens not visible to hooks (~22.6k typical)
70
+ # This includes system prompt, tools, MCP tokens that aren't in hook data
71
+ CONTEXT_BASELINE = 22_600
72
+
73
+ # Default context window size (used when not provided in hook input)
74
+ DEFAULT_CONTEXT_WINDOW = 200_000
75
+
76
+
77
+ def get_context_tokens_from_hook(hook_input: dict) -> Tuple[Optional[int], Optional[int]]:
78
+ """
79
+ Extract actual token counts from Claude Code hook input.
80
+
81
+ Claude Code provides context_window data with actual token counts:
82
+ - cache_read_input_tokens: Tokens read from cache
83
+ - input_tokens: New input tokens
84
+ - cache_creation_input_tokens: Tokens written to cache
85
+ - output_tokens: Model output tokens
86
+
87
+ Args:
88
+ hook_input: Hook input data from Claude Code
89
+
90
+ Returns:
91
+ Tuple of (tokens_used, max_tokens) or (None, None) if not available
92
+ """
93
+ context_window = hook_input.get("context_window")
94
+ if not context_window:
95
+ return None, None
96
+
97
+ current_usage = context_window.get("current_usage")
98
+ if not current_usage:
99
+ return None, None
100
+
101
+ # Sum all token types
102
+ cache_read = current_usage.get("cache_read_input_tokens", 0) or 0
103
+ input_tokens = current_usage.get("input_tokens", 0) or 0
104
+ cache_creation = current_usage.get("cache_creation_input_tokens", 0) or 0
105
+ output_tokens = current_usage.get("output_tokens", 0) or 0
106
+
107
+ content_tokens = cache_read + input_tokens + cache_creation + output_tokens
108
+
109
+ # Add baseline for system prompt, tools, MCP tokens not in hook data
110
+ tokens_used = content_tokens + CONTEXT_BASELINE
111
+
112
+ # Get max context window from hook input
113
+ max_tokens = context_window.get("context_window_size") or DEFAULT_CONTEXT_WINDOW
114
+
115
+ return tokens_used, max_tokens
116
+
117
+
118
+ def get_current_context_id(project_root: Path = None) -> Optional[str]:
119
+ """
120
+ Determine the current active context.
121
+
122
+ Falls back to most recently active context.
123
+
124
+ Returns:
125
+ Context ID or None if no active context
126
+ """
127
+ contexts = get_all_contexts(status="active", project_root=project_root)
128
+ if contexts:
129
+ return contexts[0].id # Sorted by last_active desc
130
+ return None
131
+
132
+
133
+ def get_context_warning(
134
+ percent_remaining: int,
135
+ tokens_used: int,
136
+ max_tokens: int,
137
+ context_id: Optional[str],
138
+ tool_name: str
139
+ ) -> str:
140
+ """
141
+ Generate appropriate warning based on context level.
142
+
143
+ Args:
144
+ percent_remaining: Percentage of context remaining
145
+ tokens_used: Estimated tokens used
146
+ max_tokens: Maximum context window
147
+ context_id: Current context ID (if any)
148
+ tool_name: Tool that triggered this check
149
+
150
+ Returns:
151
+ System reminder markdown
152
+ """
153
+ # Format token counts
154
+ tokens_used_k = tokens_used // 1000
155
+ max_tokens_k = max_tokens // 1000
156
+
157
+ if percent_remaining <= CRITICAL_CONTEXT_THRESHOLD:
158
+ urgency = "CRITICAL"
159
+ instruction = "You MUST wrap up immediately and create a handoff document."
160
+ else:
161
+ urgency = "LOW"
162
+ instruction = "Please wrap up your current task and prepare for handoff."
163
+
164
+ context_info = ""
165
+ if context_id:
166
+ context_info = f"""
167
+ To create a handoff document, use the /handoff command or describe:
168
+ - What you were working on
169
+ - What's completed
170
+ - What still needs to be done
171
+ - Any important decisions or context
172
+
173
+ Context ID: `{context_id}`"""
174
+
175
+ return f"""<system-reminder>
176
+ ## {urgency} CONTEXT WARNING ({percent_remaining}% remaining)
177
+
178
+ **Estimated usage**: ~{tokens_used_k}k / {max_tokens_k}k tokens
179
+ **Triggered by**: {tool_name} tool completion
180
+
181
+ {instruction}
182
+ {context_info}
183
+
184
+ **Actions:**
185
+ 1. Complete your current atomic task (if 1-2 steps away)
186
+ 2. Do NOT start new multi-step work
187
+ 3. Create a handoff document summarizing progress
188
+ 4. Ask user: "Context is getting low. I've summarized my progress. Should we continue in a new session?"
189
+ </system-reminder>"""
190
+
191
+
192
+ def check_and_transition_mode(hook_input: dict) -> None:
193
+ """
194
+ Check if context needs to transition from pending_implementation to implementing.
195
+
196
+ This handles the case where a plan was approved and implementation has started,
197
+ but the context mode wasn't updated. If we're seeing tool usage (Edit, Write, Bash)
198
+ and the context is in "pending_implementation", we transition to "implementing".
199
+
200
+ Args:
201
+ hook_input: Hook input data from Claude Code
202
+ """
203
+ # Only transition on tools that indicate implementation work
204
+ implementation_tools = {"Edit", "Write", "Bash", "NotebookEdit"}
205
+ tool_name = hook_input.get("tool_name", "")
206
+
207
+ if tool_name not in implementation_tools:
208
+ return
209
+
210
+ project_root = project_dir(hook_input)
211
+ session_id = hook_input.get("session_id")
212
+
213
+ if not session_id:
214
+ return
215
+
216
+ # Get context for this session
217
+ context = get_context_by_session_id(session_id, project_root)
218
+ if not context:
219
+ return
220
+
221
+ # Check if we need to transition
222
+ if context.in_flight and context.in_flight.mode == "pending_implementation":
223
+ eprint(f"[context_monitor] Transitioning {context.id} from pending_implementation to implementing")
224
+ update_plan_status(context.id, "implementing", project_root=project_root)
225
+
226
+
227
+ def check_context_level(hook_input: dict) -> Optional[str]:
228
+ """
229
+ Check context level and return warning if low.
230
+
231
+ Optimized for fail-fast: checks cheap conditions first before any file I/O.
232
+
233
+ Args:
234
+ hook_input: Hook input data from Claude Code
235
+
236
+ Returns:
237
+ System reminder string if context is low, None otherwise
238
+ """
239
+ # === FAST PATH: No I/O, just dict lookups and math ===
240
+
241
+ # 1. Try to get context_window data (fast dict access)
242
+ tokens_used, max_tokens = get_context_tokens_from_hook(hook_input)
243
+
244
+ # 2. If no context_window data, exit immediately - can't monitor accurately
245
+ if tokens_used is None or max_tokens is None:
246
+ # Only log if we want to debug
247
+ # eprint("[context_monitor] context_window data unavailable")
248
+ return None
249
+
250
+ # 3. Calculate percentage (fast math)
251
+ remaining = max_tokens - tokens_used
252
+ percent_remaining = max(0, min(100, int((remaining / max_tokens) * 100)))
253
+
254
+ # 4. Most common case: context is fine, exit early
255
+ if percent_remaining > LOW_CONTEXT_THRESHOLD:
256
+ return None
257
+
258
+ # === SLOW PATH: Only reached when context is low (rare) ===
259
+
260
+ # Log since we're in warning territory
261
+ eprint(f"[context_monitor] Context: {percent_remaining}% remaining "
262
+ f"(~{tokens_used//1000}k/{max_tokens//1000}k tokens)")
263
+
264
+ # Check mode transition (file I/O)
265
+ check_and_transition_mode(hook_input)
266
+
267
+ # Get current context for handoff info (file I/O)
268
+ project_root = project_dir(hook_input)
269
+ context_id = get_current_context_id(project_root)
270
+
271
+ tool_name = hook_input.get("tool_name", "Unknown")
272
+
273
+ return get_context_warning(
274
+ percent_remaining,
275
+ tokens_used,
276
+ max_tokens,
277
+ context_id,
278
+ tool_name
279
+ )
280
+
281
+
282
+ def main():
283
+ """
284
+ Main entry point for PostToolUse hook.
285
+
286
+ Reads hook input from stdin, estimates context usage,
287
+ and prints system reminder if context is low.
288
+ """
289
+ try:
290
+ # Read hook input from stdin
291
+ input_data = sys.stdin.read().strip()
292
+
293
+ if not input_data:
294
+ return
295
+
296
+ try:
297
+ hook_input = json.loads(input_data)
298
+ except json.JSONDecodeError:
299
+ return
300
+
301
+ # Check context level
302
+ warning = check_context_level(hook_input)
303
+
304
+ if warning:
305
+ # Output JSON with additionalContext so Claude sees the warning
306
+ # Plain stdout from PostToolUse only goes to verbose mode, not Claude's context
307
+ output = {
308
+ "hookSpecificOutput": {
309
+ "hookEventName": "PostToolUse",
310
+ "additionalContext": warning
311
+ }
312
+ }
313
+ print(json.dumps(output))
314
+
315
+ except Exception as e:
316
+ eprint(f"[context_monitor] ERROR: {e}")
317
+ import traceback
318
+ eprint(traceback.format_exc())
319
+
320
+
321
+ if __name__ == "__main__":
322
+ main()
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env python3
2
+ """File suggestion hook for Claude Code.
3
+
4
+ Suggests relevant files to include in context based on the current session:
5
+ - Context file (context.json) for the active context
6
+ - Plans from the active context's plans/ directory
7
+ - Handoffs from the active context's handoffs/ directory
8
+ - Reviews from the active context's reviews/ directory (including cc-native subdirectory)
9
+
10
+ Hook input (from Claude Code):
11
+ {
12
+ "session_id": "abc123",
13
+ "cwd": "/path/to/project",
14
+ ...
15
+ }
16
+
17
+ Hook output:
18
+ JSON array of file paths to suggest, or empty array if no suggestions.
19
+ ["/path/to/file1.md", "/path/to/file2.md"]
20
+ """
21
+ import json
22
+ import sys
23
+ from pathlib import Path
24
+ from typing import List, Optional
25
+
26
+ # Add parent directories to path for imports
27
+ SCRIPT_DIR = Path(__file__).resolve().parent
28
+ SHARED_LIB = SCRIPT_DIR.parent / "lib"
29
+ sys.path.insert(0, str(SHARED_LIB.parent))
30
+
31
+ from lib.base.utils import eprint, project_dir
32
+ from lib.base.constants import (
33
+ get_context_plans_dir,
34
+ get_context_handoffs_dir,
35
+ get_context_reviews_dir,
36
+ get_context_file_path,
37
+ )
38
+ from lib.context.context_manager import (
39
+ get_context_by_session_id,
40
+ get_all_in_flight_contexts,
41
+ get_context,
42
+ )
43
+
44
+
45
+ def get_context_files(context_id: str, project_root: Path) -> List[str]:
46
+ """
47
+ Get all relevant files for a context.
48
+
49
+ Collects:
50
+ - Context file (context.json)
51
+ - Plans (most recent first)
52
+ - Handoffs (most recent first)
53
+ - Reviews (most recent first)
54
+
55
+ Args:
56
+ context_id: Context identifier
57
+ project_root: Project root path
58
+
59
+ Returns:
60
+ List of absolute file paths, sorted by modification time (most recent first)
61
+ """
62
+ files = []
63
+
64
+ # Get context.json file first
65
+ context_file = get_context_file_path(context_id, project_root)
66
+ if context_file.exists():
67
+ files.append(str(context_file))
68
+ eprint(f"[file-suggestion] Found context file for {context_id}")
69
+
70
+ # Get plans directory
71
+ plans_dir = get_context_plans_dir(context_id, project_root)
72
+ if plans_dir.exists():
73
+ plan_files = list(plans_dir.glob("*.md"))
74
+ # Sort by modification time, most recent first
75
+ plan_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
76
+ files.extend([str(p) for p in plan_files])
77
+ eprint(f"[file-suggestion] Found {len(plan_files)} plans in {context_id}")
78
+
79
+ # Get handoffs directory
80
+ handoffs_dir = get_context_handoffs_dir(context_id, project_root)
81
+ if handoffs_dir.exists():
82
+ handoff_files = list(handoffs_dir.glob("*.md"))
83
+ handoff_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
84
+ files.extend([str(p) for p in handoff_files])
85
+ eprint(f"[file-suggestion] Found {len(handoff_files)} handoffs in {context_id}")
86
+
87
+ # Get reviews directory (includes cc-native subdirectory)
88
+ reviews_dir = get_context_reviews_dir(context_id, project_root)
89
+ if reviews_dir.exists():
90
+ # Find review.md files in reviews/ and subdirectories (e.g., reviews/cc-native/review.md)
91
+ review_files = list(reviews_dir.glob("**/review.md"))
92
+ review_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
93
+ files.extend([str(p) for p in review_files])
94
+ eprint(f"[file-suggestion] Found {len(review_files)} reviews in {context_id}")
95
+
96
+ return files
97
+
98
+
99
+ def get_active_context_id(session_id: str, project_root: Path) -> Optional[str]:
100
+ """
101
+ Determine the active context for suggestions.
102
+
103
+ Priority:
104
+ 1. Context bound to current session_id
105
+ 2. Single in-flight context (if only one exists)
106
+ 3. None (no suggestions if ambiguous)
107
+
108
+ Args:
109
+ session_id: Current session identifier
110
+ project_root: Project root path
111
+
112
+ Returns:
113
+ Context ID or None
114
+ """
115
+ # Try session_id lookup first
116
+ if session_id and session_id != "unknown":
117
+ context = get_context_by_session_id(session_id, project_root)
118
+ if context:
119
+ eprint(f"[file-suggestion] Found context by session: {context.id}")
120
+ return context.id
121
+
122
+ # Fall back to single in-flight context
123
+ in_flight = get_all_in_flight_contexts(project_root)
124
+ if len(in_flight) == 1:
125
+ eprint(f"[file-suggestion] Using single in-flight context: {in_flight[0].id}")
126
+ return in_flight[0].id
127
+
128
+ eprint(f"[file-suggestion] No unique context found (in-flight: {len(in_flight)})")
129
+ return None
130
+
131
+
132
+ def main():
133
+ """
134
+ Main entry point for file suggestion hook.
135
+
136
+ Reads hook input from stdin, determines active context,
137
+ and outputs file suggestions as JSON array.
138
+ """
139
+ try:
140
+ # Read hook input from stdin
141
+ input_data = sys.stdin.read().strip()
142
+
143
+ if not input_data:
144
+ print("[]")
145
+ return
146
+
147
+ try:
148
+ hook_input = json.loads(input_data)
149
+ except json.JSONDecodeError:
150
+ eprint("[file-suggestion] Failed to parse input JSON")
151
+ print("[]")
152
+ return
153
+
154
+ # Get project root and session ID
155
+ project_root = project_dir(hook_input)
156
+ session_id = hook_input.get("session_id", "unknown")
157
+
158
+ eprint(f"[file-suggestion] Session: {session_id[:8]}..., Project: {project_root}")
159
+
160
+ # Determine active context
161
+ context_id = get_active_context_id(session_id, project_root)
162
+
163
+ if not context_id:
164
+ print("[]")
165
+ return
166
+
167
+ # Collect file suggestions
168
+ suggestions = get_context_files(context_id, project_root)
169
+
170
+ # Limit suggestions to prevent overwhelming the context
171
+ MAX_SUGGESTIONS = 10
172
+ if len(suggestions) > MAX_SUGGESTIONS:
173
+ eprint(f"[file-suggestion] Limiting suggestions to {MAX_SUGGESTIONS} (was {len(suggestions)})")
174
+ suggestions = suggestions[:MAX_SUGGESTIONS]
175
+
176
+ # Output suggestions as JSON array
177
+ eprint(f"[file-suggestion] Suggesting {len(suggestions)} files")
178
+ print(json.dumps(suggestions))
179
+
180
+ except Exception as e:
181
+ eprint(f"[file-suggestion] ERROR: {e}")
182
+ import traceback
183
+ eprint(traceback.format_exc())
184
+ print("[]")
185
+
186
+
187
+ if __name__ == "__main__":
188
+ main()
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env python3
2
+ """PostToolUse hook - captures TaskCreate operations for persistence.
3
+
4
+ This hook runs after Claude uses the TaskCreate tool and automatically
5
+ records the task creation event in the context's events.jsonl.
6
+
7
+ Hook input (from Claude Code):
8
+ {
9
+ "hook_event_name": "PostToolUse",
10
+ "tool_name": "TaskCreate",
11
+ "tool_input": {
12
+ "subject": "Task subject",
13
+ "description": "Task description",
14
+ "activeForm": "Present continuous form",
15
+ "metadata": {"context": "context-id", ...}
16
+ },
17
+ "tool_response": {"task": {"id": "1", "subject": "..."}},
18
+ "session_id": "abc123",
19
+ "cwd": "/path/to/project"
20
+ }
21
+
22
+ Hook output:
23
+ - Silent on success (no stdout output)
24
+ - Logs to stderr for debugging
25
+ """
26
+ import json
27
+ import sys
28
+ from pathlib import Path
29
+ from typing import Optional, Dict, Any
30
+
31
+ # Add parent directories to path for imports
32
+ SCRIPT_DIR = Path(__file__).resolve().parent
33
+ SHARED_LIB = SCRIPT_DIR.parent / "lib"
34
+ sys.path.insert(0, str(SHARED_LIB.parent))
35
+
36
+ from lib.context.task_sync import record_task_created, generate_next_task_id
37
+ from lib.context.context_manager import get_all_contexts, get_context_by_session_id
38
+ from lib.base.utils import eprint, project_dir
39
+
40
+
41
+ def extract_context_id(
42
+ tool_input: Dict[str, Any],
43
+ project_root: Path,
44
+ session_id: Optional[str] = None
45
+ ) -> Optional[str]:
46
+ """
47
+ Extract context ID from tool input metadata, session, or active contexts.
48
+
49
+ Priority:
50
+ 1. metadata.context field
51
+ 2. Session ID lookup (session bound to context)
52
+ 3. metadata.persistent_id prefix (e.g., "ctx-123-task-1" -> "ctx-123")
53
+ 4. Single active context
54
+ 5. None (will trigger auto-creation)
55
+
56
+ Args:
57
+ tool_input: Tool input from TaskCreate
58
+ project_root: Project root directory
59
+ session_id: Session ID from hook payload
60
+
61
+ Returns:
62
+ Context ID or None if cannot determine
63
+ """
64
+ # Check metadata.context field
65
+ metadata = tool_input.get("metadata", {})
66
+ if isinstance(metadata, dict):
67
+ context = metadata.get("context")
68
+ if context:
69
+ return context
70
+
71
+ # Check session ID - session may be bound to a context
72
+ if session_id:
73
+ try:
74
+ session_context = get_context_by_session_id(session_id, project_root)
75
+ if session_context:
76
+ eprint(f"[task_create_capture] Found context via session_id: {session_context.id}")
77
+ return session_context.id
78
+ except Exception as e:
79
+ eprint(f"[task_create_capture] Failed to lookup context by session: {e}")
80
+
81
+ # Check persistent_id for context hint
82
+ if isinstance(metadata, dict):
83
+ persistent_id = metadata.get("persistent_id", "")
84
+ if persistent_id and "-" in persistent_id:
85
+ # Format: "context-id-task-1" or similar
86
+ parts = persistent_id.split("-")
87
+ if len(parts) >= 2:
88
+ # Reconstruct context ID (everything before last two parts)
89
+ context_parts = parts[:-2] if len(parts) > 2 else parts[:1]
90
+ return "-".join(context_parts)
91
+
92
+ # Check for single active context
93
+ try:
94
+ contexts = get_all_contexts(status="active", project_root=project_root)
95
+ if len(contexts) == 1:
96
+ return contexts[0].id
97
+ except Exception as e:
98
+ eprint(f"[task_create_capture] Failed to get active contexts: {e}")
99
+
100
+ return None
101
+
102
+
103
+ def main() -> int:
104
+ """
105
+ Main hook entry point.
106
+
107
+ Returns:
108
+ 0 on success, non-zero on failure (but hook is non-blocking)
109
+ """
110
+ try:
111
+ # Parse hook input
112
+ payload = json.load(sys.stdin)
113
+
114
+ # Validate hook type
115
+ if payload.get("hook_event_name") != "PostToolUse":
116
+ return 0
117
+
118
+ # Validate tool name
119
+ if payload.get("tool_name") != "TaskCreate":
120
+ return 0
121
+
122
+ # Extract tool input
123
+ tool_input = payload.get("tool_input", {})
124
+ if not isinstance(tool_input, dict):
125
+ eprint("[task_create_capture] Invalid tool_input: not a dict")
126
+ return 0
127
+
128
+ # Check for skip_persistence flag (used during hydration to avoid duplicates)
129
+ metadata = tool_input.get("metadata", {})
130
+ if isinstance(metadata, dict) and metadata.get("skip_persistence"):
131
+ eprint("[task_create_capture] Skipping persistence (hydration mode)")
132
+ return 0
133
+
134
+ # Extract tool response (contains task ID assigned by Claude)
135
+ tool_response = payload.get("tool_response", {})
136
+ if not isinstance(tool_response, dict):
137
+ eprint("[task_create_capture] Invalid tool_response: not a dict")
138
+ return 0
139
+
140
+ # Get project root and session ID
141
+ project_root = project_dir(payload)
142
+ session_id = payload.get("session_id")
143
+
144
+ # Extract context ID
145
+ context_id = extract_context_id(tool_input, project_root, session_id)
146
+ if not context_id:
147
+ eprint("[task_create_capture] No context available - skipping persistence")
148
+ eprint("[task_create_capture] Task will be ephemeral until context is created")
149
+ return 0
150
+
151
+ # Extract task data
152
+ subject = tool_input.get("subject", "")
153
+ if not subject:
154
+ eprint("[task_create_capture] Missing required field: subject")
155
+ return 0
156
+
157
+ description = tool_input.get("description", "")
158
+ active_form = tool_input.get("activeForm", "")
159
+
160
+ # Generate persistent task ID
161
+ # Claude's native ID is ephemeral (1, 2, 3...)
162
+ # We need a persistent ID that survives sessions
163
+ persistent_task_id = generate_next_task_id(context_id, project_root)
164
+
165
+ # Record the task creation event
166
+ success = record_task_created(
167
+ context_id=context_id,
168
+ task_id=persistent_task_id,
169
+ subject=subject,
170
+ description=description,
171
+ active_form=active_form,
172
+ project_root=project_root
173
+ )
174
+
175
+ if success:
176
+ eprint(f"[task_create_capture] Recorded task_added: {persistent_task_id} in {context_id}")
177
+ else:
178
+ eprint(f"[task_create_capture] Failed to record task_added: {persistent_task_id}")
179
+
180
+ # Silent success (no stdout output)
181
+ return 0
182
+
183
+ except json.JSONDecodeError as e:
184
+ eprint(f"[task_create_capture] JSON decode error: {e}")
185
+ return 0 # Non-blocking
186
+ except Exception as e:
187
+ eprint(f"[task_create_capture] Unexpected error: {e}")
188
+ import traceback
189
+ eprint(traceback.format_exc())
190
+ return 0 # Non-blocking
191
+
192
+
193
+ if __name__ == "__main__":
194
+ raise SystemExit(main())