aiwcli 0.9.8 → 0.10.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 (116) hide show
  1. package/bin/run.js +5 -2
  2. package/dist/lib/claude-settings-types.d.ts +2 -0
  3. package/dist/templates/CLAUDE.md +3 -3
  4. package/dist/templates/_shared/.claude/settings.json +4 -0
  5. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  7. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  8. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  9. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  10. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  11. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  12. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  13. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  15. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/hooks/archive_plan.py +87 -178
  17. package/dist/templates/_shared/hooks/context_monitor.py +104 -247
  18. package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
  19. package/dist/templates/_shared/hooks/pre_compact.py +47 -32
  20. package/dist/templates/_shared/hooks/session_end.py +103 -60
  21. package/dist/templates/_shared/hooks/session_start.py +110 -81
  22. package/dist/templates/_shared/hooks/task_create_capture.py +26 -50
  23. package/dist/templates/_shared/hooks/task_update_capture.py +42 -115
  24. package/dist/templates/_shared/hooks/user_prompt_submit.py +61 -61
  25. package/dist/templates/_shared/lib/base/__init__.py +16 -0
  26. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  27. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  28. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  29. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  30. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  31. package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
  32. package/dist/templates/_shared/lib/base/inference.py +121 -0
  33. package/dist/templates/_shared/lib/base/logger.py +291 -0
  34. package/dist/templates/_shared/lib/base/utils.py +42 -9
  35. package/dist/templates/_shared/lib/context/__init__.py +72 -80
  36. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  37. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  38. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  39. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  40. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  41. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  42. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  43. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  44. package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
  45. package/dist/templates/_shared/lib/context/context_selector.py +491 -0
  46. package/dist/templates/_shared/lib/context/context_store.py +636 -0
  47. package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
  48. package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
  49. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  50. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  51. package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
  52. package/dist/templates/_shared/lib/templates/README.md +5 -13
  53. package/dist/templates/_shared/lib/templates/__init__.py +2 -6
  54. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  55. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  56. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  57. package/dist/templates/_shared/lib/templates/plan_context.py +1 -38
  58. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  59. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  60. package/dist/templates/_shared/scripts/save_handoff.py +39 -19
  61. package/dist/templates/_shared/scripts/status_line.py +701 -0
  62. package/dist/templates/_shared/workflows/handoff.md +9 -3
  63. package/dist/templates/cc-native/.claude/settings.json +41 -8
  64. package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
  65. package/dist/templates/cc-native/MIGRATION.md +1 -1
  66. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
  67. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +49 -21
  68. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  69. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  70. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  71. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  72. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  73. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  74. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -55
  75. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +163 -131
  76. package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
  78. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
  79. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +6 -4
  80. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  81. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  82. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  83. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  84. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  85. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
  87. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +34 -29
  88. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  89. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  90. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  91. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  92. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  93. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
  94. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
  95. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
  96. package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
  97. package/dist/templates/cc-native/_cc-native/lib/utils.py +207 -40
  98. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
  99. package/oclif.manifest.json +1 -1
  100. package/package.json +1 -1
  101. package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
  102. package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -177
  103. package/dist/templates/_shared/lib/context/auto_state.py +0 -167
  104. package/dist/templates/_shared/lib/context/cache.py +0 -444
  105. package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
  106. package/dist/templates/_shared/lib/context/context_manager.py +0 -1057
  107. package/dist/templates/_shared/lib/context/discovery.py +0 -554
  108. package/dist/templates/_shared/lib/context/event_log.py +0 -316
  109. package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
  110. package/dist/templates/_shared/lib/context/task_sync.py +0 -407
  111. package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
  112. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  113. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  114. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  115. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
  116. package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +0 -98
@@ -2,7 +2,7 @@
2
2
  """PostToolUse hook - captures TaskCreate operations for persistence.
3
3
 
4
4
  This hook runs after Claude uses the TaskCreate tool and automatically
5
- records the task creation event in the context's events.jsonl.
5
+ records the task in the context's state.json.
6
6
 
7
7
  Hook input (from Claude Code):
8
8
  {
@@ -38,95 +38,71 @@ from lib.base.hook_utils import (
38
38
  check_skip_persistence,
39
39
  safe_hook_main,
40
40
  run_hook,
41
+ log_debug,
42
+ log_info,
43
+ log_warn,
44
+ log_error,
41
45
  )
42
- from lib.base.utils import eprint, project_dir
43
- from lib.context.context_extractor import extract_context_id
44
- from lib.context.task_sync import record_task_created, generate_next_task_id
46
+ from lib.base.utils import project_dir
47
+ from lib.context.context_store import get_context_by_session_id
48
+ from lib.context.task_tracker import add_task, generate_next_task_id
45
49
 
46
50
 
47
51
  @safe_hook_main("task_create_capture")
48
52
  def main() -> int:
49
- """
50
- Main hook entry point.
51
-
52
- Returns:
53
- 0 on success, non-zero on failure (but hook is non-blocking)
54
- """
55
- # Parse hook input
53
+ """Main hook entry point."""
56
54
  payload = load_hook_input()
57
55
  if not payload:
58
56
  return 0
59
57
 
60
- # Validate hook type and tool name
61
58
  if not validate_hook_event(payload, "PostToolUse", "TaskCreate"):
62
59
  return 0
63
60
 
64
- # Extract tool input
65
61
  tool_input = get_tool_input(payload)
66
62
  if not tool_input:
67
- eprint("[task_create_capture] Invalid tool_input: not a dict")
63
+ log_warn("task_create_capture", "Invalid tool_input: not a dict")
68
64
  return 0
69
65
 
70
- # Check for skip_persistence flag
71
66
  if check_skip_persistence(payload, "task_create_capture"):
72
67
  return 0
73
68
 
74
- # Extract tool response (contains task ID assigned by Claude)
75
- tool_response = payload.get("tool_response", {})
76
- if not isinstance(tool_response, dict):
77
- eprint("[task_create_capture] Invalid tool_response: not a dict")
78
- return 0
79
-
80
- # Get project root and session ID
81
69
  project_root = project_dir(payload)
82
- session_id = payload.get("session_id")
70
+ session_id = payload.get("session_id", "")
83
71
 
84
- # Extract context ID using unified extractor
85
- context_id = extract_context_id(
86
- tool_input,
87
- project_root,
88
- session_id=session_id,
89
- hook_name="task_create_capture",
90
- check_persistent_id=True # TaskCreate uses persistent_id for context hints
91
- )
92
- if not context_id:
93
- eprint("[task_create_capture] No context available - skipping persistence")
94
- eprint("[task_create_capture] Task will be ephemeral until context is created")
72
+ # Find context by session ID
73
+ state = get_context_by_session_id(session_id, project_root)
74
+ if not state:
75
+ log_debug("task_create_capture", "No context available - skipping persistence")
95
76
  return 0
96
77
 
78
+ context_id = state.id
79
+
97
80
  # Extract task data
98
81
  subject = tool_input.get("subject", "")
99
82
  if not subject:
100
- eprint("[task_create_capture] Missing required field: subject")
83
+ log_warn("task_create_capture", "Missing required field: subject")
101
84
  return 0
102
85
 
103
86
  description = tool_input.get("description", "")
104
87
  active_form = tool_input.get("activeForm", "")
105
88
 
106
- # Generate persistent task ID
107
- # Claude's native ID is ephemeral (1, 2, 3...)
108
- # We need a persistent ID that survives sessions
109
- persistent_task_id = generate_next_task_id(context_id, project_root)
110
-
111
- # Record the task creation event
112
- success = record_task_created(
89
+ # Add task to state.json
90
+ task = add_task(
113
91
  context_id=context_id,
114
- task_id=persistent_task_id,
115
92
  subject=subject,
116
93
  description=description,
117
94
  active_form=active_form,
118
- session_id=session_id or "",
119
- project_root=project_root
95
+ session_id=session_id,
96
+ project_root=project_root,
120
97
  )
121
98
 
122
- if success:
123
- eprint(f"[task_create_capture] Recorded task_added: {persistent_task_id} in {context_id}")
99
+ if task:
100
+ log_info("task_create_capture", f"Recorded task: {task['id']} in {context_id}")
124
101
  else:
125
- eprint(f"[task_create_capture] Failed to record task_added: {persistent_task_id}")
102
+ log_error("task_create_capture", f"Failed to add task in {context_id}")
126
103
 
127
- # Silent success (no stdout output)
128
104
  return 0
129
105
 
130
106
 
131
107
  if __name__ == "__main__":
132
- run_hook(main)
108
+ run_hook(main, "task_create_capture")
@@ -2,13 +2,7 @@
2
2
  """PostToolUse hook - captures TaskUpdate operations for persistence.
3
3
 
4
4
  This hook runs after Claude uses the TaskUpdate tool and automatically
5
- records the appropriate event in the context's events.jsonl based on the
6
- status change.
7
-
8
- Status mappings:
9
- - status: "in_progress" -> record_task_started()
10
- - status: "completed" -> record_task_completed()
11
- - blockedBy added -> record_task_blocked()
5
+ records the update in the context's state.json.
12
6
 
13
7
  Hook input (from Claude Code):
14
8
  {
@@ -46,173 +40,106 @@ from lib.base.hook_utils import (
46
40
  check_skip_persistence,
47
41
  safe_hook_main,
48
42
  run_hook,
43
+ log_debug,
44
+ log_info,
45
+ log_warn,
46
+ log_error,
49
47
  )
50
- from lib.base.utils import eprint, project_dir
51
- from lib.context.context_extractor import extract_context_id
52
- from lib.context.task_sync import (
53
- record_task_started,
54
- record_task_completed,
55
- record_task_blocked,
56
- record_task_deleted,
57
- )
48
+ from lib.base.utils import project_dir
49
+ from lib.context.context_store import get_context_by_session_id
50
+ from lib.context.task_tracker import update_task, delete_task
58
51
 
59
52
 
60
53
  def get_persistent_task_id(
61
54
  claude_task_id: str,
62
55
  tool_input: Dict[str, Any]
63
56
  ) -> str:
64
- """
65
- Convert Claude's ephemeral task ID to persistent task ID.
66
-
67
- If metadata.persistent_id exists, use that.
68
- Otherwise, assume format "aiw-{claude_task_id}".
69
-
70
- Args:
71
- claude_task_id: Task ID from Claude (e.g., "1", "2")
72
- tool_input: Tool input dict
73
-
74
- Returns:
75
- Persistent task ID (e.g., "aiw-1")
76
- """
57
+ """Convert Claude's ephemeral task ID to persistent task ID."""
77
58
  metadata = tool_input.get("metadata", {})
78
59
  if isinstance(metadata, dict):
79
60
  persistent_id = metadata.get("persistent_id")
80
61
  if persistent_id:
81
62
  return persistent_id
82
-
83
- # Default: aiw-{id}
84
63
  return f"aiw-{claude_task_id}"
85
64
 
86
65
 
87
66
  @safe_hook_main("task_update_capture")
88
67
  def main() -> int:
89
- """
90
- Main hook entry point.
91
-
92
- Returns:
93
- 0 on success, non-zero on failure (but hook is non-blocking)
94
- """
95
- # Parse hook input
68
+ """Main hook entry point."""
96
69
  payload = load_hook_input()
97
70
  if not payload:
98
71
  return 0
99
72
 
100
- # Validate hook type and tool name
101
73
  if not validate_hook_event(payload, "PostToolUse", "TaskUpdate"):
102
74
  return 0
103
75
 
104
- # Extract tool input
105
76
  tool_input = get_tool_input(payload)
106
77
  if not tool_input:
107
- eprint("[task_update_capture] Invalid tool_input: not a dict")
78
+ log_warn("task_update_capture", "Invalid tool_input: not a dict")
108
79
  return 0
109
80
 
110
- # Check for skip_persistence flag
111
81
  if check_skip_persistence(payload, "task_update_capture"):
112
82
  return 0
113
83
 
114
- # Get project root and session ID
115
84
  project_root = project_dir(payload)
116
- session_id = payload.get("session_id")
117
-
118
- # Extract context ID using unified extractor
119
- context_id = extract_context_id(
120
- tool_input,
121
- project_root,
122
- session_id=session_id,
123
- hook_name="task_update_capture"
124
- )
125
- if not context_id:
126
- eprint("[task_update_capture] No context available - skipping persistence")
85
+ session_id = payload.get("session_id", "")
86
+
87
+ # Find context by session ID
88
+ state = get_context_by_session_id(session_id, project_root)
89
+ if not state:
90
+ log_debug("task_update_capture", "No context available - skipping persistence")
127
91
  return 0
128
92
 
93
+ context_id = state.id
94
+
129
95
  # Extract task ID
130
96
  claude_task_id = tool_input.get("taskId")
131
97
  if not claude_task_id:
132
- eprint("[task_update_capture] Missing required field: taskId")
98
+ log_warn("task_update_capture", "Missing required field: taskId")
133
99
  return 0
134
100
 
135
- # Get persistent task ID
136
101
  persistent_task_id = get_persistent_task_id(claude_task_id, tool_input)
137
102
 
138
- # Check for status change
139
103
  status = tool_input.get("status")
140
104
  metadata = tool_input.get("metadata", {})
141
- add_blocked_by = tool_input.get("addBlockedBy", [])
142
105
 
143
- # Handle different update types
144
106
  events_recorded = []
145
107
 
146
- # Status: in_progress
147
- if status == "in_progress":
148
- success = record_task_started(
149
- context_id=context_id,
150
- task_id=persistent_task_id,
151
- session_id=session_id or "",
152
- project_root=project_root
153
- )
154
- if success:
155
- events_recorded.append("task_started")
156
-
157
- # Status: completed
158
- elif status == "completed":
159
- # Extract rich completion context from metadata
108
+ if status == "deleted":
109
+ if delete_task(context_id, persistent_task_id, project_root):
110
+ events_recorded.append("task_deleted")
111
+ elif status:
112
+ # Extract completion metadata
113
+ evidence = ""
114
+ work_summary = ""
115
+ files_changed = None
160
116
  if isinstance(metadata, dict):
161
- evidence = metadata.get("evidence", "Task marked completed")
117
+ evidence = metadata.get("evidence", "")
162
118
  work_summary = metadata.get("work_summary", "")
163
- files_changed = metadata.get("files_changed", [])
164
- commit_ref = metadata.get("commit_ref", "")
165
- else:
166
- evidence = "Task marked completed"
167
- work_summary = ""
168
- files_changed = []
169
- commit_ref = ""
170
-
171
- success = record_task_completed(
119
+ files_changed = metadata.get("files_changed")
120
+ if files_changed and not isinstance(files_changed, list):
121
+ files_changed = None
122
+
123
+ success = update_task(
172
124
  context_id=context_id,
173
125
  task_id=persistent_task_id,
126
+ status=status,
174
127
  evidence=evidence,
175
128
  work_summary=work_summary,
176
- files_changed=files_changed if isinstance(files_changed, list) else [],
177
- commit_ref=commit_ref,
178
- session_id=session_id or "",
179
- project_root=project_root
180
- )
181
- if success:
182
- events_recorded.append("task_completed")
183
-
184
- # Status: deleted
185
- elif status == "deleted":
186
- success = record_task_deleted(
187
- context_id=context_id,
188
- task_id=persistent_task_id,
189
- session_id=session_id or "",
190
- project_root=project_root
191
- )
192
- if success:
193
- events_recorded.append("task_deleted")
194
-
195
- # Blocked by tasks
196
- if add_blocked_by and isinstance(add_blocked_by, list) and len(add_blocked_by) > 0:
197
- blocked_reason = f"Blocked by tasks: {', '.join(add_blocked_by)}"
198
- success = record_task_blocked(
199
- context_id=context_id,
200
- task_id=persistent_task_id,
201
- reason=blocked_reason,
202
- session_id=session_id or "",
203
- project_root=project_root
129
+ files_changed=files_changed,
130
+ session_id=session_id,
131
+ project_root=project_root,
204
132
  )
205
133
  if success:
206
- events_recorded.append("task_blocked")
134
+ events_recorded.append(f"task_{status}")
207
135
 
208
136
  if events_recorded:
209
- eprint(f"[task_update_capture] Recorded {', '.join(events_recorded)} for {persistent_task_id} in {context_id}")
137
+ log_info("task_update_capture", f"Recorded {', '.join(events_recorded)} for {persistent_task_id} in {context_id}")
210
138
  else:
211
- eprint(f"[task_update_capture] No relevant status changes detected for {persistent_task_id}")
139
+ log_debug("task_update_capture", f"No relevant changes for {persistent_task_id}")
212
140
 
213
- # Silent success (no stdout output)
214
141
  return 0
215
142
 
216
143
 
217
144
  if __name__ == "__main__":
218
- run_hook(main)
145
+ run_hook(main, "task_update_capture")
@@ -28,72 +28,56 @@ SCRIPT_DIR = Path(__file__).resolve().parent
28
28
  SHARED_LIB = SCRIPT_DIR.parent / "lib"
29
29
  sys.path.insert(0, str(SHARED_LIB.parent))
30
30
 
31
- from lib.base.hook_utils import load_hook_input
32
- from lib.base.utils import eprint, project_dir
33
- from lib.context.context_manager import (
34
- update_context_session_id,
35
- update_plan_status,
31
+ from lib.base.hook_utils import load_hook_input, log_debug, log_info, log_warn, log_error, log_diagnostic
32
+ from lib.base.utils import project_dir
33
+ from lib.context.context_store import (
36
34
  get_context,
37
35
  get_context_by_session_id,
36
+ bind_session,
37
+ maybe_activate,
38
+ save_state,
38
39
  )
39
-
40
- # Import the enforcement module
41
- from hooks.context_enforcer import determine_context, BlockRequest
40
+ from lib.context.context_selector import determine_context, BlockRequest
42
41
 
43
42
 
44
43
  def format_claudemd_reminder() -> str:
45
44
  """Generate reminder to update directory-specific CLAUDE.md files."""
46
45
  return """
47
- ## CLAUDE.md Decision Capture
48
-
49
- **Placement rule:** CLAUDE.md files cascade — subdirectories inherit from parents. Default to updating the nearest existing CLAUDE.md. Only create a new one at a semantic boundary where responsibility genuinely shifts (package roots with their own manifest, technology boundaries, domain boundaries, integration points).
50
-
51
- **Before creating a new CLAUDE.md, ask:** "Do the rules here actually differ from the parent?" If not, update the parent instead.
46
+ ## CLAUDE.md \u2014 Persistent Memory
52
47
 
53
- **Keep files lean (progressive disclosure):** Each CLAUDE.md should contain only decisions relevant at that directory level. Verbose files become noise that degrades future task quality every entry competes for attention in the context window. Capture only non-obvious rationale, not descriptions of what the code does.
48
+ CLAUDE.md files are this project's persistent memory across sessions. **After making a significant decision or learning something non-obvious during this task, write it to the nearest CLAUDE.md.** If you don't write it, it's lost when this session ends.
54
49
 
55
- **What to capture** (only when decisions have non-obvious rationale):
56
- - Architectural choices and their alternatives
50
+ **What to write:**
51
+ - Architectural choices and why alternatives were rejected
57
52
  - Non-obvious constraints (what breaks if this changes)
58
53
  - Workarounds with context on the underlying issue
59
- - Learned patterns that prevent future mistakes
54
+ - Patterns that prevent future mistakes
55
+
56
+ **Placement:** CLAUDE.md files cascade \u2014 subdirectories inherit from parents. Update the nearest existing one. Only create a new one at a genuine semantic boundary (package root, technology boundary, domain boundary).
60
57
 
61
58
  **Format:**
62
59
 
63
60
  ```markdown
64
61
  ## [Topic]
65
62
  **Decision:** [What was decided]
66
- **Rationale:** [Why the non-obvious part]
63
+ **Rationale:** [Why \u2014 the non-obvious part]
67
64
  ```
65
+
66
+ **When in doubt, write it.** A lean entry is better than a lost decision.
68
67
  """
69
68
 
70
69
 
71
70
  def _update_in_flight_status(context_id: str, hook_input: dict, project_root: Path) -> None:
72
71
  """
73
- Update context in-flight status based on permission mode.
72
+ Update context mode based on permission mode.
74
73
 
75
- - If permission_mode == "plan": set to "planning"
76
- - If permission_mode in ["acceptEdits", "bypassPermissions"]: set to "implementing"
74
+ - permission_mode == "plan": no-op (planning is runtime-only, not persisted)
75
+ - permission_mode != "plan" and mode == "idle": set to "active"
76
+ - permission_mode != "plan" and mode == "has_plan": set to "active" (plan was accepted)
77
77
  """
78
- context = get_context(context_id, project_root)
79
- if not context or not context.in_flight:
80
- return
81
-
82
- current_mode = context.in_flight.mode
83
78
  permission_mode = hook_input.get("permission_mode", "default")
84
- eprint(f"[user_prompt_submit] Current mode: {current_mode}, permission_mode: {permission_mode}")
85
-
86
- # Set status based on permission mode
87
- if permission_mode == "plan":
88
- if current_mode != "planning":
89
- update_plan_status(context_id, "planning", project_root=project_root)
90
- eprint(f"[user_prompt_submit] Set status to 'planning'")
91
- elif permission_mode != "plan":
92
- # Any non-plan permission mode transitions pending/planning to implementing
93
- # This includes "default" (after /clear) and "acceptEdits"/"bypassPermissions"
94
- if current_mode in ["pending_implementation", "planning", "none"]:
95
- update_plan_status(context_id, "implementing", project_root=project_root)
96
- eprint(f"[user_prompt_submit] Set status to 'implementing' (permission_mode={permission_mode})")
79
+ log_debug("user_prompt_submit", f"context_id={context_id}, permission_mode={permission_mode}")
80
+ maybe_activate(context_id, permission_mode, project_root=project_root, caller="user_prompt_submit")
97
81
 
98
82
 
99
83
  def main():
@@ -104,70 +88,86 @@ def main():
104
88
  Uses session_id to detect first prompt vs subsequent prompts.
105
89
  """
106
90
  try:
107
- # Read hook input using shared utility
108
91
  hook_input = load_hook_input()
109
-
110
92
  if not hook_input:
111
93
  return
112
94
 
113
- # Get user prompt and project root
114
95
  user_prompt = hook_input.get("prompt", "")
115
96
  project_root = project_dir(hook_input)
116
97
  session_id = hook_input.get("session_id", "unknown")
117
98
 
99
+ log_diagnostic("user_prompt_submit", "receive", f"session={session_id[:8]}, prompt_len={len(user_prompt)}",
100
+ inputs={"session_id": session_id[:12], "prompt_length": len(user_prompt)})
101
+
118
102
  outputs: List[str] = []
119
- active_context_id = None # Track context for CLAUDE.md reminder
103
+ active_context_id = None
120
104
 
121
105
  # First-prompt detection: check if session_id is already bound to a context
122
106
  existing_context = get_context_by_session_id(session_id, project_root)
123
107
 
124
108
  if existing_context:
125
109
  # NOT first prompt - session already bound to context
126
- # Skip expensive context detection
127
- eprint(f"[user_prompt_submit] Session {session_id[:8]}... already bound to {existing_context.id}")
128
- # Still update in-flight status based on permission mode
110
+ log_debug("user_prompt_submit", f"Session {session_id[:8]}... already bound to {existing_context.id}")
111
+ log_diagnostic("user_prompt_submit", "decide", f"Session already bound to {existing_context.id}",
112
+ decision="session_match", reasoning="session_id found in existing context",
113
+ inputs={"context_id": existing_context.id, "mode": existing_context.mode})
129
114
  _update_in_flight_status(existing_context.id, hook_input, project_root)
130
115
  active_context_id = existing_context.id
131
116
  elif user_prompt:
132
117
  # FIRST prompt - need context detection
133
118
  try:
134
- context_id, method, context_output = determine_context(user_prompt, project_root, session_id)
135
- eprint(f"[user_prompt_submit] Context: {method} -> {context_id}")
119
+ context_id, method, context_output = determine_context(user_prompt, session_id, project_root)
120
+ log_info("user_prompt_submit", f"Context: {method} -> {context_id}")
121
+ log_diagnostic("user_prompt_submit", "decide", f"Context detected via {method}: {context_id}",
122
+ decision=method, reasoning=f"determine_context returned method={method}",
123
+ inputs={"context_id": context_id, "has_output": bool(context_output)})
136
124
 
137
125
  if context_id:
138
126
  # Bind session to context
139
- update_context_session_id(context_id, session_id, project_root)
140
- eprint(f"[user_prompt_submit] Bound session {session_id[:8]}... to context '{context_id}'")
127
+ bind_session(context_id, session_id, project_root)
128
+ log_info("user_prompt_submit", f"Bound session {session_id[:8]}... to context '{context_id}'")
141
129
 
142
- # Update in-flight status based on permission mode
130
+ # Update mode based on permission mode
143
131
  _update_in_flight_status(context_id, hook_input, project_root)
144
132
  active_context_id = context_id
145
133
 
134
+ # Clear handoff_path after it's been injected via context_selector
135
+ try:
136
+ ctx = get_context(context_id, project_root)
137
+ if ctx and ctx.handoff_path:
138
+ ctx.handoff_path = None
139
+ save_state(ctx, project_root)
140
+ log_debug("user_prompt_submit", f"Cleared handoff_path for {context_id}")
141
+ except Exception as e:
142
+ log_warn("user_prompt_submit", f"Failed to clear handoff_path: {e}")
143
+
146
144
  if context_output:
147
145
  outputs.append(context_output)
148
146
 
149
147
  except BlockRequest as e:
150
- # Block the request - print to stderr and exit with code 2
151
- # This shows the context picker to the user
152
- print(e.message, file=sys.stderr)
148
+ log_error("user_prompt_submit", e.message)
153
149
  sys.exit(2)
154
150
 
155
- # Inject CLAUDE.md reminder when in implementing mode
151
+ # Inject CLAUDE.md reminder when in active mode
156
152
  if active_context_id:
157
153
  context = get_context(active_context_id, project_root)
158
- if context and context.in_flight and context.in_flight.mode == "implementing":
154
+ if context and context.mode == "active":
159
155
  outputs.append(f"<system-reminder>{format_claudemd_reminder()}</system-reminder>")
160
- eprint(f"[user_prompt_submit] Injected CLAUDE.md reminder (mode=implementing)")
156
+ log_debug("user_prompt_submit", "Injected CLAUDE.md reminder (mode=active)")
157
+
158
+ log_diagnostic("user_prompt_submit", "result", f"context={active_context_id}, outputs={len(outputs)}",
159
+ inputs={"active_context_id": active_context_id, "output_count": len(outputs)})
161
160
 
162
- # Print output
163
161
  if outputs:
164
162
  print("\n\n".join(outputs))
165
163
 
166
164
  except Exception as e:
167
- eprint(f"[user_prompt_submit] ERROR: {e}")
168
165
  import traceback
169
- eprint(traceback.format_exc())
166
+ tb = traceback.format_exc()
167
+ from lib.base.hook_utils import log_hook_error
168
+ log_hook_error("user_prompt_submit", e, "UserPromptSubmit", traceback_str=tb)
170
169
 
171
170
 
172
171
  if __name__ == "__main__":
173
- main()
172
+ from lib.base.hook_utils import run_hook
173
+ run_hook(main, "user_prompt_submit")
@@ -23,6 +23,15 @@ from .utils import (
23
23
  sanitize_title,
24
24
  generate_context_id,
25
25
  )
26
+ from .logger import (
27
+ hook_log,
28
+ log_debug,
29
+ log_info,
30
+ log_warn,
31
+ log_error,
32
+ log_hook_error,
33
+ set_context_path,
34
+ )
26
35
 
27
36
  __all__ = [
28
37
  "atomic_write",
@@ -46,4 +55,11 @@ __all__ = [
46
55
  "sanitize_filename",
47
56
  "sanitize_title",
48
57
  "generate_context_id",
58
+ "hook_log",
59
+ "log_debug",
60
+ "log_info",
61
+ "log_warn",
62
+ "log_error",
63
+ "log_hook_error",
64
+ "set_context_path",
49
65
  ]