aiwcli 0.9.7 → 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 (119) 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 +49 -18
  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_atomicity.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  15. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  17. package/dist/templates/_shared/hooks/archive_plan.py +87 -178
  18. package/dist/templates/_shared/hooks/context_monitor.py +128 -194
  19. package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
  20. package/dist/templates/_shared/hooks/pre_compact.py +104 -0
  21. package/dist/templates/_shared/hooks/session_end.py +154 -0
  22. package/dist/templates/_shared/hooks/session_start.py +145 -59
  23. package/dist/templates/_shared/hooks/task_create_capture.py +26 -49
  24. package/dist/templates/_shared/hooks/task_update_capture.py +42 -100
  25. package/dist/templates/_shared/hooks/user_prompt_submit.py +63 -77
  26. package/dist/templates/_shared/lib/base/__init__.py +16 -0
  27. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  28. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  29. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  30. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  31. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  32. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  33. package/dist/templates/_shared/lib/base/constants.py +18 -4
  34. package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
  35. package/dist/templates/_shared/lib/base/inference.py +121 -0
  36. package/dist/templates/_shared/lib/base/logger.py +291 -0
  37. package/dist/templates/_shared/lib/base/utils.py +49 -11
  38. package/dist/templates/_shared/lib/context/__init__.py +72 -80
  39. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  40. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  41. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  42. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  43. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  44. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  45. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  46. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  47. package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
  48. package/dist/templates/_shared/lib/context/context_selector.py +491 -0
  49. package/dist/templates/_shared/lib/context/context_store.py +636 -0
  50. package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
  51. package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
  52. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  53. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  54. package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
  55. package/dist/templates/_shared/lib/templates/README.md +5 -13
  56. package/dist/templates/_shared/lib/templates/__init__.py +2 -6
  57. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  58. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  59. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  60. package/dist/templates/_shared/lib/templates/plan_context.py +25 -79
  61. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  62. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  63. package/dist/templates/_shared/scripts/save_handoff.py +39 -19
  64. package/dist/templates/_shared/scripts/status_line.py +701 -0
  65. package/dist/templates/_shared/workflows/handoff.md +9 -3
  66. package/dist/templates/cc-native/.claude/settings.json +64 -9
  67. package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
  68. package/dist/templates/cc-native/MIGRATION.md +1 -1
  69. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
  70. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -1
  71. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +57 -22
  72. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  73. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  74. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  75. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  76. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  78. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -57
  79. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +208 -158
  80. package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
  81. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
  82. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
  83. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +35 -10
  84. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  85. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  87. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  88. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  89. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  90. package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
  91. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +103 -42
  92. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  93. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  94. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  95. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  96. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  97. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
  98. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
  99. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
  100. package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
  101. package/dist/templates/cc-native/_cc-native/lib/utils.py +210 -43
  102. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
  103. package/oclif.manifest.json +1 -1
  104. package/package.json +1 -1
  105. package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
  106. package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -205
  107. package/dist/templates/_shared/lib/context/cache.py +0 -444
  108. package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
  109. package/dist/templates/_shared/lib/context/context_manager.py +0 -1054
  110. package/dist/templates/_shared/lib/context/discovery.py +0 -444
  111. package/dist/templates/_shared/lib/context/event_log.py +0 -308
  112. package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
  113. package/dist/templates/_shared/lib/context/task_sync.py +0 -290
  114. package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
  115. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  116. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  117. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  118. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
  119. package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +0 -98
@@ -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,158 +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
- )
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
57
51
 
58
52
 
59
53
  def get_persistent_task_id(
60
54
  claude_task_id: str,
61
55
  tool_input: Dict[str, Any]
62
56
  ) -> str:
63
- """
64
- Convert Claude's ephemeral task ID to persistent task ID.
65
-
66
- If metadata.persistent_id exists, use that.
67
- Otherwise, assume format "aiw-{claude_task_id}".
68
-
69
- Args:
70
- claude_task_id: Task ID from Claude (e.g., "1", "2")
71
- tool_input: Tool input dict
72
-
73
- Returns:
74
- Persistent task ID (e.g., "aiw-1")
75
- """
57
+ """Convert Claude's ephemeral task ID to persistent task ID."""
76
58
  metadata = tool_input.get("metadata", {})
77
59
  if isinstance(metadata, dict):
78
60
  persistent_id = metadata.get("persistent_id")
79
61
  if persistent_id:
80
62
  return persistent_id
81
-
82
- # Default: aiw-{id}
83
63
  return f"aiw-{claude_task_id}"
84
64
 
85
65
 
86
66
  @safe_hook_main("task_update_capture")
87
67
  def main() -> int:
88
- """
89
- Main hook entry point.
90
-
91
- Returns:
92
- 0 on success, non-zero on failure (but hook is non-blocking)
93
- """
94
- # Parse hook input
68
+ """Main hook entry point."""
95
69
  payload = load_hook_input()
96
70
  if not payload:
97
71
  return 0
98
72
 
99
- # Validate hook type and tool name
100
73
  if not validate_hook_event(payload, "PostToolUse", "TaskUpdate"):
101
74
  return 0
102
75
 
103
- # Extract tool input
104
76
  tool_input = get_tool_input(payload)
105
77
  if not tool_input:
106
- eprint("[task_update_capture] Invalid tool_input: not a dict")
78
+ log_warn("task_update_capture", "Invalid tool_input: not a dict")
107
79
  return 0
108
80
 
109
- # Check for skip_persistence flag
110
81
  if check_skip_persistence(payload, "task_update_capture"):
111
82
  return 0
112
83
 
113
- # Get project root and session ID
114
84
  project_root = project_dir(payload)
115
- session_id = payload.get("session_id")
116
-
117
- # Extract context ID using unified extractor
118
- context_id = extract_context_id(
119
- tool_input,
120
- project_root,
121
- session_id=session_id,
122
- hook_name="task_update_capture"
123
- )
124
- if not context_id:
125
- 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")
126
91
  return 0
127
92
 
93
+ context_id = state.id
94
+
128
95
  # Extract task ID
129
96
  claude_task_id = tool_input.get("taskId")
130
97
  if not claude_task_id:
131
- eprint("[task_update_capture] Missing required field: taskId")
98
+ log_warn("task_update_capture", "Missing required field: taskId")
132
99
  return 0
133
100
 
134
- # Get persistent task ID
135
101
  persistent_task_id = get_persistent_task_id(claude_task_id, tool_input)
136
102
 
137
- # Check for status change
138
103
  status = tool_input.get("status")
139
104
  metadata = tool_input.get("metadata", {})
140
- add_blocked_by = tool_input.get("addBlockedBy", [])
141
105
 
142
- # Handle different update types
143
106
  events_recorded = []
144
107
 
145
- # Status: in_progress
146
- if status == "in_progress":
147
- success = record_task_started(
148
- context_id=context_id,
149
- task_id=persistent_task_id,
150
- project_root=project_root
151
- )
152
- if success:
153
- events_recorded.append("task_started")
154
-
155
- # Status: completed
156
- elif status == "completed":
157
- # 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
158
116
  if isinstance(metadata, dict):
159
- evidence = metadata.get("evidence", "Task marked completed")
117
+ evidence = metadata.get("evidence", "")
160
118
  work_summary = metadata.get("work_summary", "")
161
- files_changed = metadata.get("files_changed", [])
162
- commit_ref = metadata.get("commit_ref", "")
163
- else:
164
- evidence = "Task marked completed"
165
- work_summary = ""
166
- files_changed = []
167
- commit_ref = ""
168
-
169
- 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(
170
124
  context_id=context_id,
171
125
  task_id=persistent_task_id,
126
+ status=status,
172
127
  evidence=evidence,
173
128
  work_summary=work_summary,
174
- files_changed=files_changed if isinstance(files_changed, list) else [],
175
- commit_ref=commit_ref,
176
- project_root=project_root
177
- )
178
- if success:
179
- events_recorded.append("task_completed")
180
-
181
- # Blocked by tasks
182
- if add_blocked_by and isinstance(add_blocked_by, list) and len(add_blocked_by) > 0:
183
- blocked_reason = f"Blocked by tasks: {', '.join(add_blocked_by)}"
184
- success = record_task_blocked(
185
- context_id=context_id,
186
- task_id=persistent_task_id,
187
- reason=blocked_reason,
188
- project_root=project_root
129
+ files_changed=files_changed,
130
+ session_id=session_id,
131
+ project_root=project_root,
189
132
  )
190
133
  if success:
191
- events_recorded.append("task_blocked")
134
+ events_recorded.append(f"task_{status}")
192
135
 
193
136
  if events_recorded:
194
- 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}")
195
138
  else:
196
- 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}")
197
140
 
198
- # Silent success (no stdout output)
199
141
  return 0
200
142
 
201
143
 
202
144
  if __name__ == "__main__":
203
- run_hook(main)
145
+ run_hook(main, "task_update_capture")
@@ -28,84 +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
46
+ ## CLAUDE.md \u2014 Persistent Memory
47
+
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.
48
49
 
49
- When implementing changes, consider whether this work involves decisions with non-obvious rationale. If so, update or create a CLAUDE.md in the relevant directory.
50
+ **What to write:**
51
+ - Architectural choices and why alternatives were rejected
52
+ - Non-obvious constraints (what breaks if this changes)
53
+ - Workarounds with context on the underlying issue
54
+ - Patterns that prevent future mistakes
50
55
 
51
- **When to update CLAUDE.md:**
52
- - Architectural choices (why this pattern over alternatives)
53
- - Non-obvious constraints (why something MUST be done a certain way)
54
- - Learned patterns (discovered issues that future work should avoid)
55
- - Integration decisions (why components connect this way)
56
- - Workarounds (temporary solutions with context on the underlying issue)
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).
57
57
 
58
- **What to capture (use this format):**
58
+ **Format:**
59
59
 
60
60
  ```markdown
61
61
  ## [Topic]
62
-
63
62
  **Decision:** [What was decided]
64
- **Rationale:** [Why this approach was chosen]
65
- **Constraint:** [What breaks if this changes]
63
+ **Rationale:** [Why \u2014 the non-obvious part]
66
64
  ```
67
65
 
68
- **Directory-specific:** Place CLAUDE.md in the directory closest to the affected code. If no CLAUDE.md exists, create one with a descriptive header.
69
-
70
- **Example new CLAUDE.md:**
71
-
72
- ```markdown
73
- # Component Name
74
-
75
- Development decisions and patterns for this component.
76
-
77
- ## [First Decision Topic]
78
- ...
79
- ```
66
+ **When in doubt, write it.** A lean entry is better than a lost decision.
80
67
  """
81
68
 
82
69
 
83
70
  def _update_in_flight_status(context_id: str, hook_input: dict, project_root: Path) -> None:
84
71
  """
85
- Update context in-flight status based on permission mode.
72
+ Update context mode based on permission mode.
86
73
 
87
- - If permission_mode == "plan": set to "planning"
88
- - 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)
89
77
  """
90
- context = get_context(context_id, project_root)
91
- if not context or not context.in_flight:
92
- return
93
-
94
- current_mode = context.in_flight.mode
95
78
  permission_mode = hook_input.get("permission_mode", "default")
96
- eprint(f"[user_prompt_submit] Current mode: {current_mode}, permission_mode: {permission_mode}")
97
-
98
- # Set status based on permission mode
99
- if permission_mode == "plan":
100
- if current_mode != "planning":
101
- update_plan_status(context_id, "planning", project_root=project_root)
102
- eprint(f"[user_prompt_submit] Set status to 'planning'")
103
- elif permission_mode != "plan":
104
- # Any non-plan permission mode transitions pending/planning to implementing
105
- # This includes "default" (after /clear) and "acceptEdits"/"bypassPermissions"
106
- if current_mode in ["pending_implementation", "planning", "none"]:
107
- update_plan_status(context_id, "implementing", project_root=project_root)
108
- 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")
109
81
 
110
82
 
111
83
  def main():
@@ -116,72 +88,86 @@ def main():
116
88
  Uses session_id to detect first prompt vs subsequent prompts.
117
89
  """
118
90
  try:
119
- # Read hook input using shared utility
120
91
  hook_input = load_hook_input()
121
-
122
92
  if not hook_input:
123
93
  return
124
94
 
125
- # Get user prompt and project root
126
95
  user_prompt = hook_input.get("prompt", "")
127
96
  project_root = project_dir(hook_input)
128
97
  session_id = hook_input.get("session_id", "unknown")
129
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
+
130
102
  outputs: List[str] = []
131
- active_context_id = None # Track context for CLAUDE.md reminder
103
+ active_context_id = None
132
104
 
133
105
  # First-prompt detection: check if session_id is already bound to a context
134
106
  existing_context = get_context_by_session_id(session_id, project_root)
135
107
 
136
108
  if existing_context:
137
109
  # NOT first prompt - session already bound to context
138
- # Skip expensive context detection
139
- eprint(f"[user_prompt_submit] Session {session_id[:8]}... already bound to {existing_context.id}")
140
- # 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})
141
114
  _update_in_flight_status(existing_context.id, hook_input, project_root)
142
115
  active_context_id = existing_context.id
143
116
  elif user_prompt:
144
117
  # FIRST prompt - need context detection
145
118
  try:
146
- context_id, method, context_output, remaining_prompt = determine_context(user_prompt, project_root, session_id)
147
- eprint(f"[user_prompt_submit] Context: {method} -> {context_id}")
148
- if remaining_prompt:
149
- eprint(f"[user_prompt_submit] Actual request: {remaining_prompt[:50]}...")
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)})
150
124
 
151
125
  if context_id:
152
126
  # Bind session to context
153
- update_context_session_id(context_id, session_id, project_root)
154
- 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}'")
155
129
 
156
- # Update in-flight status based on permission mode
130
+ # Update mode based on permission mode
157
131
  _update_in_flight_status(context_id, hook_input, project_root)
158
132
  active_context_id = context_id
159
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
+
160
144
  if context_output:
161
145
  outputs.append(context_output)
162
146
 
163
147
  except BlockRequest as e:
164
- # Block the request - print to stderr and exit with code 2
165
- # This shows the context picker to the user
166
- print(e.message, file=sys.stderr)
148
+ log_error("user_prompt_submit", e.message)
167
149
  sys.exit(2)
168
150
 
169
- # Inject CLAUDE.md reminder when in implementing mode
151
+ # Inject CLAUDE.md reminder when in active mode
170
152
  if active_context_id:
171
153
  context = get_context(active_context_id, project_root)
172
- if context and context.in_flight and context.in_flight.mode == "implementing":
154
+ if context and context.mode == "active":
173
155
  outputs.append(f"<system-reminder>{format_claudemd_reminder()}</system-reminder>")
174
- 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)})
175
160
 
176
- # Print output
177
161
  if outputs:
178
162
  print("\n\n".join(outputs))
179
163
 
180
164
  except Exception as e:
181
- eprint(f"[user_prompt_submit] ERROR: {e}")
182
165
  import traceback
183
- 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)
184
169
 
185
170
 
186
171
  if __name__ == "__main__":
187
- 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
  ]
@@ -15,7 +15,7 @@ from pathlib import Path
15
15
  # Directory names (relative to project root)
16
16
  OUTPUT_DIR = "_output"
17
17
  CONTEXTS_DIR = "contexts"
18
- ARCHIVE_DIR = "archive"
18
+ ARCHIVE_DIR = "_archive"
19
19
  INDEX_FILENAME = "index.json"
20
20
 
21
21
  # Context ID validation
@@ -255,6 +255,20 @@ def get_events_file_path(context_id: str, project_root: Path = None) -> Path:
255
255
  return get_context_dir(context_id, project_root) / "events.jsonl"
256
256
 
257
257
 
258
+ def get_auto_state_path(context_id: str, project_root: Path = None) -> Path:
259
+ """
260
+ Get the auto-state.json file path for a specific context.
261
+
262
+ Args:
263
+ context_id: Context identifier
264
+ project_root: Project root directory (default: cwd)
265
+
266
+ Returns:
267
+ Path to _output/contexts/{context_id}/auto-state.json
268
+ """
269
+ return get_context_dir(context_id, project_root) / "auto-state.json"
270
+
271
+
258
272
  def get_archive_dir(project_root: Path = None) -> Path:
259
273
  """
260
274
  Get the archive directory path.
@@ -263,7 +277,7 @@ def get_archive_dir(project_root: Path = None) -> Path:
263
277
  project_root: Project root directory (default: cwd)
264
278
 
265
279
  Returns:
266
- Path to _output/contexts/archive/
280
+ Path to _output/contexts/_archive/
267
281
  """
268
282
  return get_contexts_dir(project_root) / ARCHIVE_DIR
269
283
 
@@ -277,7 +291,7 @@ def get_archive_context_dir(context_id: str, project_root: Path = None) -> Path:
277
291
  project_root: Project root directory (default: cwd)
278
292
 
279
293
  Returns:
280
- Path to _output/contexts/archive/{context_id}/
294
+ Path to _output/contexts/_archive/{context_id}/
281
295
 
282
296
  Raises:
283
297
  ValueError: If context_id is invalid
@@ -294,7 +308,7 @@ def get_archive_index_path(project_root: Path = None) -> Path:
294
308
  project_root: Project root directory (default: cwd)
295
309
 
296
310
  Returns:
297
- Path to _output/contexts/archive/index.json
311
+ Path to _output/contexts/_archive/index.json
298
312
  """
299
313
  return get_archive_dir(project_root) / INDEX_FILENAME
300
314