aiwcli 0.9.8 → 0.10.1

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 (115) 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 +114 -60
  21. package/dist/templates/_shared/hooks/session_start.py +127 -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 +47 -81
  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 +207 -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 +317 -0
  45. package/dist/templates/_shared/lib/context/context_selector.py +508 -0
  46. package/dist/templates/_shared/lib/context/context_store.py +653 -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 +22 -37
  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 +31 -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 +37 -14
  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 +54 -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 +76 -89
  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_questions_early.py +81 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
  78. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +6 -4
  79. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  80. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  81. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  82. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  83. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  84. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  85. package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
  86. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +34 -29
  87. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  88. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  89. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  90. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  91. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  92. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
  93. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
  94. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
  95. package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
  96. package/dist/templates/cc-native/_cc-native/lib/utils.py +207 -40
  97. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
  98. package/oclif.manifest.json +1 -1
  99. package/package.json +1 -1
  100. package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
  101. package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -177
  102. package/dist/templates/_shared/lib/context/auto_state.py +0 -167
  103. package/dist/templates/_shared/lib/context/cache.py +0 -444
  104. package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
  105. package/dist/templates/_shared/lib/context/context_manager.py +0 -1057
  106. package/dist/templates/_shared/lib/context/discovery.py +0 -554
  107. package/dist/templates/_shared/lib/context/event_log.py +0 -316
  108. package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
  109. package/dist/templates/_shared/lib/context/task_sync.py +0 -407
  110. package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
  111. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  112. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  113. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  114. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
  115. 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,29 @@ 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
42
-
43
-
44
- def format_claudemd_reminder() -> str:
45
- """Generate reminder to update directory-specific CLAUDE.md files."""
46
- 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.
52
-
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.
54
-
55
- **What to capture** (only when decisions have non-obvious rationale):
56
- - Architectural choices and their alternatives
57
- - Non-obvious constraints (what breaks if this changes)
58
- - Workarounds with context on the underlying issue
59
- - Learned patterns that prevent future mistakes
60
-
61
- **Format:**
62
-
63
- ```markdown
64
- ## [Topic]
65
- **Decision:** [What was decided]
66
- **Rationale:** [Why — the non-obvious part]
67
- ```
68
- """
40
+ from lib.context.context_selector import determine_context, BlockRequest
69
41
 
70
42
 
71
43
  def _update_in_flight_status(context_id: str, hook_input: dict, project_root: Path) -> None:
72
44
  """
73
- Update context in-flight status based on permission mode.
45
+ Update context mode based on permission mode.
74
46
 
75
- - If permission_mode == "plan": set to "planning"
76
- - If permission_mode in ["acceptEdits", "bypassPermissions"]: set to "implementing"
47
+ - permission_mode == "plan": no-op (planning is runtime-only, not persisted)
48
+ - permission_mode != "plan" and mode == "idle": set to "active"
49
+ - permission_mode != "plan" and mode == "has_plan": set to "active" (plan was accepted)
77
50
  """
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
51
  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})")
52
+ log_debug("user_prompt_submit", f"context_id={context_id}, permission_mode={permission_mode}")
53
+ maybe_activate(context_id, permission_mode, project_root=project_root, caller="user_prompt_submit")
97
54
 
98
55
 
99
56
  def main():
@@ -104,70 +61,79 @@ def main():
104
61
  Uses session_id to detect first prompt vs subsequent prompts.
105
62
  """
106
63
  try:
107
- # Read hook input using shared utility
108
64
  hook_input = load_hook_input()
109
-
110
65
  if not hook_input:
111
66
  return
112
67
 
113
- # Get user prompt and project root
114
68
  user_prompt = hook_input.get("prompt", "")
115
69
  project_root = project_dir(hook_input)
116
70
  session_id = hook_input.get("session_id", "unknown")
117
71
 
72
+ log_diagnostic("user_prompt_submit", "receive", f"session={session_id[:8]}, prompt_len={len(user_prompt)}",
73
+ inputs={"session_id": session_id[:12], "prompt_length": len(user_prompt)})
74
+
118
75
  outputs: List[str] = []
119
- active_context_id = None # Track context for CLAUDE.md reminder
76
+ active_context_id = None
120
77
 
121
78
  # First-prompt detection: check if session_id is already bound to a context
122
79
  existing_context = get_context_by_session_id(session_id, project_root)
123
80
 
124
81
  if existing_context:
125
82
  # 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
83
+ log_debug("user_prompt_submit", f"Session {session_id[:8]}... already bound to {existing_context.id}")
84
+ log_diagnostic("user_prompt_submit", "decide", f"Session already bound to {existing_context.id}",
85
+ decision="session_match", reasoning="session_id found in existing context",
86
+ inputs={"context_id": existing_context.id, "mode": existing_context.mode})
129
87
  _update_in_flight_status(existing_context.id, hook_input, project_root)
130
88
  active_context_id = existing_context.id
131
89
  elif user_prompt:
132
90
  # FIRST prompt - need context detection
133
91
  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}")
92
+ context_id, method, context_output = determine_context(user_prompt, session_id, project_root)
93
+ log_info("user_prompt_submit", f"Context: {method} -> {context_id}")
94
+ log_diagnostic("user_prompt_submit", "decide", f"Context detected via {method}: {context_id}",
95
+ decision=method, reasoning=f"determine_context returned method={method}",
96
+ inputs={"context_id": context_id, "has_output": bool(context_output)})
136
97
 
137
98
  if context_id:
138
99
  # 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}'")
100
+ bind_session(context_id, session_id, project_root)
101
+ log_info("user_prompt_submit", f"Bound session {session_id[:8]}... to context '{context_id}'")
141
102
 
142
- # Update in-flight status based on permission mode
103
+ # Update mode based on permission mode
143
104
  _update_in_flight_status(context_id, hook_input, project_root)
144
105
  active_context_id = context_id
145
106
 
107
+ # Clear handoff_path after it's been injected via context_selector
108
+ try:
109
+ ctx = get_context(context_id, project_root)
110
+ if ctx and ctx.handoff_path:
111
+ ctx.handoff_path = None
112
+ save_state(ctx, project_root)
113
+ log_debug("user_prompt_submit", f"Cleared handoff_path for {context_id}")
114
+ except Exception as e:
115
+ log_warn("user_prompt_submit", f"Failed to clear handoff_path: {e}")
116
+
146
117
  if context_output:
147
118
  outputs.append(context_output)
148
119
 
149
120
  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)
121
+ log_error("user_prompt_submit", e.message)
153
122
  sys.exit(2)
154
123
 
155
- # Inject CLAUDE.md reminder when in implementing mode
156
- if active_context_id:
157
- context = get_context(active_context_id, project_root)
158
- if context and context.in_flight and context.in_flight.mode == "implementing":
159
- outputs.append(f"<system-reminder>{format_claudemd_reminder()}</system-reminder>")
160
- eprint(f"[user_prompt_submit] Injected CLAUDE.md reminder (mode=implementing)")
124
+ log_diagnostic("user_prompt_submit", "result", f"context={active_context_id}, outputs={len(outputs)}",
125
+ inputs={"active_context_id": active_context_id, "output_count": len(outputs)})
161
126
 
162
- # Print output
163
127
  if outputs:
164
128
  print("\n\n".join(outputs))
165
129
 
166
130
  except Exception as e:
167
- eprint(f"[user_prompt_submit] ERROR: {e}")
168
131
  import traceback
169
- eprint(traceback.format_exc())
132
+ tb = traceback.format_exc()
133
+ from lib.base.hook_utils import log_hook_error
134
+ log_hook_error("user_prompt_submit", e, "UserPromptSubmit", traceback_str=tb)
170
135
 
171
136
 
172
137
  if __name__ == "__main__":
173
- main()
138
+ from lib.base.hook_utils import run_hook
139
+ 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
  ]