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
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env python3
2
+ """SessionEnd hook - saves session state to state.json.
3
+
4
+ Fires when session terminates (quit, /clear, logout). Saves last_session
5
+ data directly to state.json for restoration on next session.
6
+
7
+ Hook input (from Claude Code):
8
+ {
9
+ "hook_event_name": "SessionEnd",
10
+ "session_id": "abc123",
11
+ "source": "prompt_input_exit", # or "clear", "logout", "compact"
12
+ "transcript_path": "/path/to/transcript.jsonl",
13
+ "cwd": "/path/to/project",
14
+ ...
15
+ }
16
+
17
+ Hook output:
18
+ - Silent (no stdout output needed for SessionEnd)
19
+ - Logs to stderr for debugging
20
+ """
21
+ import hashlib
22
+ import subprocess
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ # Add parent directories to path for imports
27
+ SCRIPT_DIR = Path(__file__).resolve().parent
28
+ SHARED_LIB = SCRIPT_DIR.parent / "lib"
29
+ sys.path.insert(0, str(SHARED_LIB.parent))
30
+
31
+ from lib.base.hook_utils import load_hook_input, log_debug, log_info, log_warn, log_error, log_diagnostic
32
+ from lib.base.utils import now_iso, project_dir
33
+ from lib.context.context_store import get_context_by_session_id, save_state
34
+ from lib.context.plan_manager import find_latest_plan, normalize_plan_content, generate_plan_id, extract_plan_anchors
35
+
36
+
37
+ def _get_git_state(project_root: Path) -> dict:
38
+ """Capture current git state for restoration."""
39
+ git_state = {}
40
+ try:
41
+ result = subprocess.run(
42
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
43
+ capture_output=True, text=True, cwd=str(project_root), timeout=5,
44
+ )
45
+ if result.returncode == 0:
46
+ git_state["branch"] = result.stdout.strip()
47
+
48
+ result = subprocess.run(
49
+ ["git", "diff", "--name-only"],
50
+ capture_output=True, text=True, cwd=str(project_root), timeout=5,
51
+ )
52
+ if result.returncode == 0:
53
+ files = [f for f in result.stdout.strip().split("\n") if f]
54
+ git_state["uncommitted_files"] = files
55
+
56
+ result = subprocess.run(
57
+ ["git", "log", "-1", "--oneline"],
58
+ capture_output=True, text=True, cwd=str(project_root), timeout=5,
59
+ )
60
+ if result.returncode == 0:
61
+ git_state["last_commit_short"] = result.stdout.strip()
62
+ except Exception as e:
63
+ log_warn("session_end", f"Git state capture error (non-fatal): {e}")
64
+
65
+ return git_state
66
+
67
+
68
+ def main():
69
+ """Save session state to state.json."""
70
+ try:
71
+ hook_input = load_hook_input()
72
+ if not hook_input:
73
+ return
74
+
75
+ session_id = hook_input.get("session_id", "")
76
+ source = hook_input.get("source", "other")
77
+ transcript_path = hook_input.get("transcript_path")
78
+ project_root = project_dir(hook_input)
79
+
80
+ if not session_id:
81
+ log_debug("session_end", "No session_id, skipping")
82
+ return
83
+
84
+ log_info("session_end", f"Session ending: {session_id[:8]}... reason={source}")
85
+ log_diagnostic("session_end", "receive", f"session={session_id[:8]}, source={source}",
86
+ inputs={"session_id": session_id[:12], "source": source})
87
+
88
+ # Find context bound to this session
89
+ state = get_context_by_session_id(session_id, project_root)
90
+ if not state:
91
+ log_debug("session_end", "No context bound to this session, skipping")
92
+ return
93
+
94
+ log_info("session_end", f"Found context: {state.id}")
95
+
96
+ # Capture git state
97
+ git_state = _get_git_state(project_root)
98
+
99
+ # Save last_session directly to state.json
100
+ state.last_session = {
101
+ "session_id": session_id,
102
+ "save_reason": source,
103
+ "saved_at": now_iso(),
104
+ "transcript_path": transcript_path,
105
+ "git_state": git_state,
106
+ }
107
+ state.last_active = now_iso()
108
+
109
+ # Fallback: assign plan fields if PostToolUse:ExitPlanMode didn't fire.
110
+ # When ExitPlanMode triggers /clear, the session terminates before PostToolUse
111
+ # hooks can run, so plan_accepted.py never fires. Detect this by checking
112
+ # for an archived plan that hasn't been assigned yet.
113
+ if not state.plan_hash:
114
+ latest_plan_path = find_latest_plan(state.id, project_root)
115
+ if latest_plan_path:
116
+ try:
117
+ content = Path(latest_plan_path).read_text(encoding="utf-8")
118
+ normalized = normalize_plan_content(content)
119
+ state.plan_hash = hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:12]
120
+ state.plan_path = latest_plan_path
121
+ state.plan_signature = content[:200]
122
+ state.plan_id = generate_plan_id()
123
+ state.plan_anchors = extract_plan_anchors(content)
124
+ log_info("session_end", f"Fallback: assigned archived plan for {state.id} (hash: {state.plan_hash})")
125
+ except Exception as e:
126
+ log_warn("session_end", f"Fallback plan assignment failed: {e}")
127
+
128
+ # If a plan is assigned and mode is active, stage it for next session
129
+ if state.plan_hash and state.mode == "active":
130
+ state.mode = "has_plan"
131
+ log_info("session_end", f"Staged plan for next session: {state.id} -> has_plan")
132
+ log_diagnostic("session_end", "decide", f"Staging plan for {state.id}",
133
+ decision="stage_plan", reasoning="plan_hash exists and mode was active",
134
+ inputs={"plan_hash": state.plan_hash, "mode_transition": "active->has_plan"})
135
+
136
+ if save_state(state, project_root):
137
+ log_info("session_end", f"Saved last_session for {state.id}")
138
+ log_diagnostic("session_end", "result", f"Saved state for {state.id}",
139
+ decision="saved", inputs={"context_id": state.id, "mode": state.mode,
140
+ "has_plan_hash": bool(state.plan_hash),
141
+ "git_files": len(git_state.get("uncommitted_files", []))})
142
+ else:
143
+ log_error("session_end", f"Failed to save state for {state.id}")
144
+
145
+ except Exception as e:
146
+ import traceback
147
+ tb = traceback.format_exc()
148
+ from lib.base.hook_utils import log_hook_error
149
+ log_hook_error("session_end", e, "SessionEnd", traceback_str=tb)
150
+
151
+
152
+ if __name__ == "__main__":
153
+ from lib.base.hook_utils import run_hook
154
+ run_hook(main, "session_end")
@@ -1,18 +1,17 @@
1
1
  #!/usr/bin/env python3
2
- """SessionStart hook for mode transitions after /clear.
2
+ """SessionStart hook for post-compaction and post-clear restore.
3
3
 
4
- This hook fires when a new session starts. It handles the critical transition
5
- from `pending_implementation` to `implementing` when a session starts after
6
- /clear with bypass permissions.
4
+ This hook fires when a new session starts. It handles:
7
5
 
8
- The flow is:
9
- 1. User approves plan (ExitPlanMode) -> mode = pending_implementation
10
- 2. User clicks "yes and clear and bypass permissions"
11
- 3. SessionStart fires with source="clear" and permission_mode="bypassPermissions"
12
- 4. This hook transitions mode to "implementing"
6
+ 1. Post-clear restore (source="clear"): After ExitPlanMode "clear context",
7
+ Claude Code runs /clear and auto-pastes the plan. The auto-paste bypasses
8
+ all hooks (UserPromptSubmit never fires), so this hook bridges the gap:
9
+ find the has_plan context (set by session_end moments ago), bind the new
10
+ session, transition has_plan active, and inject restoration context.
13
11
 
14
- Without this hook, the mode stays stuck at pending_implementation because
15
- UserPromptSubmit may not receive the correct permission_mode after /clear.
12
+ 2. Post-compaction restore (source="compact"): The session is already bound
13
+ to a context. Load state and inject rich restoration context so Claude
14
+ can continue seamlessly after compaction.
16
15
 
17
16
  Hook input:
18
17
  {
@@ -32,26 +31,127 @@ SCRIPT_DIR = Path(__file__).resolve().parent
32
31
  SHARED_LIB = SCRIPT_DIR.parent / "lib"
33
32
  sys.path.insert(0, str(SHARED_LIB.parent))
34
33
 
35
- from lib.base.hook_utils import load_hook_input
36
- from lib.base.utils import eprint, project_dir
37
- from lib.context.context_manager import (
38
- get_all_in_flight_contexts,
39
- update_plan_status,
40
- update_context_session_id,
41
- )
34
+ from lib.base.hook_utils import emit_context, load_hook_input, log_debug, log_info, log_error, log_diagnostic
35
+ from lib.base.utils import project_dir
36
+ from lib.context.context_store import get_context_by_session_id, get_all_contexts, bind_session, update_mode
37
+ from lib.context.context_formatter import _build_restore_sections
42
38
 
43
39
 
44
- def main():
40
+ def _handle_compact_restore(hook_input, session_id, project_root):
41
+ """
42
+ Handle post-compaction restore.
43
+
44
+ After compaction, the session is already bound to a context.
45
+ Load state and inject rich restoration context via additionalContext.
45
46
  """
46
- Handle mode transitions on session start.
47
+ state = get_context_by_session_id(session_id, project_root)
48
+ if not state:
49
+ log_debug("session_start", "No context bound to session after compact")
50
+ return
51
+
52
+ log_info("session_start", f"Post-compaction restore for context: {state.id}")
53
+
54
+ # Build restoration context
55
+ mode_display = state.mode.replace("_", " ").title() if state.mode != "idle" else "Active"
56
+
57
+ lines = [
58
+ f"## Resuming Context After Compaction: {state.id}",
59
+ "",
60
+ f"**Summary:** {state.summary}",
61
+ f"**Mode:** {mode_display}",
62
+ ]
63
+
64
+ # Add restore sections (tasks, git state, plan content)
65
+ # inline_plan=True because plan content is NOT auto-pasted after compaction
66
+ restore = _build_restore_sections(state, project_root, inline_plan=True)
67
+ if restore:
68
+ lines.append(restore)
69
+
70
+ lines.extend([
71
+ "",
72
+ "---",
73
+ "",
74
+ "**Instructions:**",
75
+ "Context was compacted to free memory. Your previous conversation has been summarized.",
76
+ "1. Review the previous work above",
77
+ "2. Continue from where you left off",
78
+ ])
79
+
80
+ restore_context = "\n".join(lines)
81
+ emit_context(restore_context)
82
+ log_info("session_start", f"Injected post-compaction restore context for {state.id}")
83
+ log_diagnostic("session_start", "result", f"Compact restore complete for {state.id}",
84
+ decision="injected", inputs={"context_id": state.id, "mode": state.mode})
85
+
86
+
87
+ def _handle_clear_restore(hook_input, session_id, project_root):
88
+ """
89
+ Handle plan context restoration after /clear.
90
+
91
+ After ExitPlanMode "clear context", Claude Code auto-pastes the plan
92
+ but the auto-paste bypasses all hooks — UserPromptSubmit never fires.
93
+ This means the new session is never bound to a context.
47
94
 
48
- When source is "clear" and permission_mode is "bypassPermissions" or "acceptEdits",
49
- transition any pending_implementation context to implementing.
95
+ Fix: find the has_plan context (set by session_end moments ago),
96
+ bind the new session to it, and inject restoration context.
50
97
  """
98
+ # Find has_plan contexts (sorted by last_active descending)
99
+ has_plan = [
100
+ c for c in get_all_contexts(status="active", project_root=project_root)
101
+ if c.mode == "has_plan"
102
+ ]
103
+
104
+ if not has_plan:
105
+ log_debug("session_start", "No has_plan contexts found after /clear")
106
+ return
107
+
108
+ # Pick the most recently active one (first in list, already sorted)
109
+ target = has_plan[0]
110
+ log_info("session_start", f"Found has_plan context after /clear: {target.id}")
111
+
112
+ # Bind new session to this context
113
+ bind_session(target.id, session_id, project_root)
114
+ log_info("session_start", f"Bound session {session_id[:8]}... to {target.id}")
115
+
116
+ # Transition has_plan → active (consume the transient state)
117
+ update_mode(target.id, "active", project_root=project_root)
118
+ log_info("session_start", f"Transitioned {target.id}: has_plan -> active")
119
+
120
+ # Inject restoration context (tasks, git state, plan path reference)
121
+ # Plan CONTENT is not injected — Claude Code auto-pastes it after /clear
122
+ mode_display = "Active (Plan Restored)"
123
+ lines = [
124
+ f"## Resuming Context After Plan Clear: {target.id}",
125
+ "",
126
+ f"**Summary:** {target.summary}",
127
+ f"**Mode:** {mode_display}",
128
+ ]
129
+
130
+ restore = _build_restore_sections(target, project_root)
131
+ if restore:
132
+ lines.append(restore)
133
+
134
+ lines.extend([
135
+ "",
136
+ "---",
137
+ "",
138
+ "**Instructions:**",
139
+ "Context was cleared for plan implementation. Your plan content has been pasted above.",
140
+ "1. Review the plan content above",
141
+ "2. Implement the plan step by step",
142
+ ])
143
+
144
+ restore_context = "\n".join(lines)
145
+ emit_context(restore_context)
146
+ log_info("session_start", f"Injected clear-restore context for {target.id}")
147
+ log_diagnostic("session_start", "result", f"Clear restore complete for {target.id}",
148
+ decision="injected", inputs={"context_id": target.id, "mode_transition": "has_plan->active"})
149
+
150
+
151
+ def main():
152
+ """Handle post-compaction and post-clear restore on session start."""
51
153
  try:
52
- # Read hook input using shared utility
53
154
  hook_input = load_hook_input()
54
-
55
155
  if not hook_input:
56
156
  return
57
157
 
@@ -60,44 +160,30 @@ def main():
60
160
  session_id = hook_input.get("session_id", "unknown")
61
161
  project_root = project_dir(hook_input)
62
162
 
63
- eprint(f"[session_start] source={source}, permission_mode={permission_mode}, session={session_id[:8]}...")
64
-
65
- # Only handle /clear with bypass/accept permissions
66
- if source != "clear":
67
- eprint(f"[session_start] Skipping: source is '{source}', not 'clear'")
68
- return
69
-
70
- if permission_mode == "plan":
71
- eprint(f"[session_start] Skipping: permission_mode is 'plan' (in planning mode)")
72
- return
73
-
74
- # Find contexts in pending_implementation mode
75
- in_flight_contexts = get_all_in_flight_contexts(project_root)
76
- pending_contexts = [
77
- ctx for ctx in in_flight_contexts
78
- if ctx.in_flight and ctx.in_flight.mode == "pending_implementation"
79
- ]
80
-
81
- if not pending_contexts:
82
- eprint("[session_start] No pending_implementation contexts found")
83
- return
84
-
85
- # Transition each pending context to implementing
86
- for ctx in pending_contexts:
87
- eprint(f"[session_start] Transitioning {ctx.id} from pending_implementation to implementing")
88
- update_plan_status(ctx.id, "implementing", project_root=project_root)
89
-
90
- # Also bind this session to the context
91
- update_context_session_id(ctx.id, session_id, project_root)
92
- eprint(f"[session_start] Bound session {session_id[:8]}... to context {ctx.id}")
93
-
94
- eprint(f"[session_start] Transitioned {len(pending_contexts)} context(s) to implementing")
163
+ log_info("session_start", f"source={source}, permission_mode={permission_mode}, session={session_id[:8]}...")
164
+ log_diagnostic("session_start", "receive", f"source={source}, session={session_id[:8]}",
165
+ inputs={"source": source, "session_id": session_id[:12], "permission_mode": permission_mode})
166
+
167
+ if source == "compact":
168
+ log_diagnostic("session_start", "decide", "Taking compact restore path",
169
+ decision="compact_restore", reasoning="source=compact")
170
+ _handle_compact_restore(hook_input, session_id, project_root)
171
+ elif source == "clear":
172
+ log_diagnostic("session_start", "decide", "Taking clear restore path",
173
+ decision="clear_restore", reasoning="source=clear, looking for has_plan context")
174
+ _handle_clear_restore(hook_input, session_id, project_root)
175
+ else:
176
+ log_diagnostic("session_start", "decide", f"No action for source={source}",
177
+ decision="skip", reasoning=f"source={source} has no handler")
178
+ log_debug("session_start", f"No action for source='{source}'")
95
179
 
96
180
  except Exception as e:
97
- eprint(f"[session_start] ERROR: {e}")
98
181
  import traceback
99
- eprint(traceback.format_exc())
182
+ tb = traceback.format_exc()
183
+ from lib.base.hook_utils import log_hook_error
184
+ log_hook_error("session_start", e, "SessionStart", traceback_str=tb)
100
185
 
101
186
 
102
187
  if __name__ == "__main__":
103
- main()
188
+ from lib.base.hook_utils import run_hook
189
+ run_hook(main, "session_start")
@@ -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,94 +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
- project_root=project_root
95
+ session_id=session_id,
96
+ project_root=project_root,
119
97
  )
120
98
 
121
- if success:
122
- 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}")
123
101
  else:
124
- 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}")
125
103
 
126
- # Silent success (no stdout output)
127
104
  return 0
128
105
 
129
106
 
130
107
  if __name__ == "__main__":
131
- run_hook(main)
108
+ run_hook(main, "task_create_capture")