aiwcli 0.9.8 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/run.js +5 -2
- package/dist/lib/claude-settings-types.d.ts +2 -0
- package/dist/templates/CLAUDE.md +3 -3
- package/dist/templates/_shared/.claude/settings.json +4 -0
- package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/archive_plan.py +87 -178
- package/dist/templates/_shared/hooks/context_monitor.py +104 -247
- package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
- package/dist/templates/_shared/hooks/pre_compact.py +47 -32
- package/dist/templates/_shared/hooks/session_end.py +103 -60
- package/dist/templates/_shared/hooks/session_start.py +110 -81
- package/dist/templates/_shared/hooks/task_create_capture.py +26 -50
- package/dist/templates/_shared/hooks/task_update_capture.py +42 -115
- package/dist/templates/_shared/hooks/user_prompt_submit.py +61 -61
- package/dist/templates/_shared/lib/base/__init__.py +16 -0
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
- package/dist/templates/_shared/lib/base/inference.py +121 -0
- package/dist/templates/_shared/lib/base/logger.py +291 -0
- package/dist/templates/_shared/lib/base/utils.py +42 -9
- package/dist/templates/_shared/lib/context/__init__.py +72 -80
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
- package/dist/templates/_shared/lib/context/context_selector.py +491 -0
- package/dist/templates/_shared/lib/context/context_store.py +636 -0
- package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
- package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
- package/dist/templates/_shared/lib/templates/README.md +5 -13
- package/dist/templates/_shared/lib/templates/__init__.py +2 -6
- package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +1 -38
- package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.py +39 -19
- package/dist/templates/_shared/scripts/status_line.py +701 -0
- package/dist/templates/_shared/workflows/handoff.md +9 -3
- package/dist/templates/cc-native/.claude/settings.json +41 -8
- package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
- package/dist/templates/cc-native/MIGRATION.md +1 -1
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +49 -21
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -55
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +163 -131
- package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +6 -4
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +34 -29
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
- package/dist/templates/cc-native/_cc-native/lib/utils.py +207 -40
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
- package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -177
- package/dist/templates/_shared/lib/context/auto_state.py +0 -167
- package/dist/templates/_shared/lib/context/cache.py +0 -444
- package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
- package/dist/templates/_shared/lib/context/context_manager.py +0 -1057
- package/dist/templates/_shared/lib/context/discovery.py +0 -554
- package/dist/templates/_shared/lib/context/event_log.py +0 -316
- package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
- package/dist/templates/_shared/lib/context/task_sync.py +0 -407
- package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
- package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +0 -98
|
@@ -26,11 +26,9 @@ SCRIPT_DIR = Path(__file__).resolve().parent
|
|
|
26
26
|
SHARED_LIB = SCRIPT_DIR.parent / "lib"
|
|
27
27
|
sys.path.insert(0, str(SHARED_LIB.parent))
|
|
28
28
|
|
|
29
|
-
from lib.base.hook_utils import load_hook_input
|
|
30
|
-
from lib.base.utils import
|
|
31
|
-
from lib.context.
|
|
32
|
-
from lib.context.event_log import EVENT_AUTO_STATE_SAVED, append_event
|
|
33
|
-
from lib.context.auto_state import save_auto_state
|
|
29
|
+
from lib.base.hook_utils import load_hook_input, log_debug, log_info, log_error
|
|
30
|
+
from lib.base.utils import project_dir
|
|
31
|
+
from lib.context.context_store import get_context_by_session_id, save_state
|
|
34
32
|
|
|
35
33
|
|
|
36
34
|
def main():
|
|
@@ -45,45 +43,62 @@ def main():
|
|
|
45
43
|
project_root = project_dir(hook_input)
|
|
46
44
|
|
|
47
45
|
if not session_id:
|
|
48
|
-
|
|
46
|
+
log_debug("pre_compact", "No session_id, skipping")
|
|
49
47
|
return
|
|
50
48
|
|
|
51
|
-
|
|
49
|
+
log_info("pre_compact", f"Saving state before compaction: {session_id[:8]}...")
|
|
52
50
|
|
|
53
51
|
# Find context bound to this session
|
|
54
52
|
context = get_context_by_session_id(session_id, project_root)
|
|
55
53
|
if not context:
|
|
56
|
-
|
|
54
|
+
log_debug("pre_compact", "No context bound to this session, skipping")
|
|
57
55
|
return
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
session_id=session_id,
|
|
67
|
-
save_reason="pre_compact",
|
|
68
|
-
project_root=project_root,
|
|
69
|
-
in_flight_mode=in_flight_mode,
|
|
70
|
-
plan_path=plan_path,
|
|
71
|
-
handoff_path=handoff_path,
|
|
72
|
-
transcript_path=transcript_path,
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
if saved:
|
|
76
|
-
append_event(
|
|
77
|
-
context_id, EVENT_AUTO_STATE_SAVED, project_root,
|
|
78
|
-
session_id=session_id, save_reason="pre_compact",
|
|
57
|
+
# Save last_session snapshot directly to state.json
|
|
58
|
+
import subprocess
|
|
59
|
+
git_state = {}
|
|
60
|
+
try:
|
|
61
|
+
branch = subprocess.run(
|
|
62
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
63
|
+
capture_output=True, text=True, timeout=5
|
|
79
64
|
)
|
|
80
|
-
|
|
65
|
+
git_state["branch"] = branch.stdout.strip() if branch.returncode == 0 else "unknown"
|
|
66
|
+
|
|
67
|
+
status = subprocess.run(
|
|
68
|
+
["git", "status", "--short"],
|
|
69
|
+
capture_output=True, text=True, timeout=5
|
|
70
|
+
)
|
|
71
|
+
if status.returncode == 0 and status.stdout.strip():
|
|
72
|
+
git_state["uncommitted_files"] = [
|
|
73
|
+
line.split(None, 1)[-1] for line in status.stdout.strip().split("\n")[:10]
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
log = subprocess.run(
|
|
77
|
+
["git", "log", "-1", "--format=%h %s"],
|
|
78
|
+
capture_output=True, text=True, timeout=5
|
|
79
|
+
)
|
|
80
|
+
if log.returncode == 0:
|
|
81
|
+
git_state["last_commit_short"] = log.stdout.strip()
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
from lib.base.utils import now_iso
|
|
86
|
+
context.last_session = {
|
|
87
|
+
"session_id": session_id,
|
|
88
|
+
"saved_at": now_iso(),
|
|
89
|
+
"save_reason": "pre_compact",
|
|
90
|
+
"git_state": git_state,
|
|
91
|
+
}
|
|
92
|
+
save_state(context, project_root)
|
|
93
|
+
log_info("pre_compact", f"State saved for {context.id}")
|
|
81
94
|
|
|
82
95
|
except Exception as e:
|
|
83
|
-
eprint(f"[pre_compact] ERROR: {e}")
|
|
84
96
|
import traceback
|
|
85
|
-
|
|
97
|
+
tb = traceback.format_exc()
|
|
98
|
+
from lib.base.hook_utils import log_hook_error
|
|
99
|
+
log_hook_error("pre_compact", e, "PreCompact", traceback_str=tb)
|
|
86
100
|
|
|
87
101
|
|
|
88
102
|
if __name__ == "__main__":
|
|
89
|
-
|
|
103
|
+
from lib.base.hook_utils import run_hook
|
|
104
|
+
run_hook(main, "pre_compact")
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""SessionEnd hook -
|
|
2
|
+
"""SessionEnd hook - saves session state to state.json.
|
|
3
3
|
|
|
4
|
-
Fires when session terminates (quit, /clear, logout).
|
|
5
|
-
|
|
4
|
+
Fires when session terminates (quit, /clear, logout). Saves last_session
|
|
5
|
+
data directly to state.json for restoration on next session.
|
|
6
6
|
|
|
7
7
|
Hook input (from Claude Code):
|
|
8
8
|
{
|
|
@@ -18,6 +18,8 @@ Hook output:
|
|
|
18
18
|
- Silent (no stdout output needed for SessionEnd)
|
|
19
19
|
- Logs to stderr for debugging
|
|
20
20
|
"""
|
|
21
|
+
import hashlib
|
|
22
|
+
import subprocess
|
|
21
23
|
import sys
|
|
22
24
|
from pathlib import Path
|
|
23
25
|
|
|
@@ -26,16 +28,45 @@ SCRIPT_DIR = Path(__file__).resolve().parent
|
|
|
26
28
|
SHARED_LIB = SCRIPT_DIR.parent / "lib"
|
|
27
29
|
sys.path.insert(0, str(SHARED_LIB.parent))
|
|
28
30
|
|
|
29
|
-
from lib.base.hook_utils import load_hook_input
|
|
30
|
-
from lib.base.utils import
|
|
31
|
-
from lib.context.
|
|
32
|
-
from lib.context.
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
35
66
|
|
|
36
67
|
|
|
37
68
|
def main():
|
|
38
|
-
"""
|
|
69
|
+
"""Save session state to state.json."""
|
|
39
70
|
try:
|
|
40
71
|
hook_input = load_hook_input()
|
|
41
72
|
if not hook_input:
|
|
@@ -47,65 +78,77 @@ def main():
|
|
|
47
78
|
project_root = project_dir(hook_input)
|
|
48
79
|
|
|
49
80
|
if not session_id:
|
|
50
|
-
|
|
81
|
+
log_debug("session_end", "No session_id, skipping")
|
|
51
82
|
return
|
|
52
83
|
|
|
53
|
-
|
|
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})
|
|
54
87
|
|
|
55
88
|
# Find context bound to this session
|
|
56
|
-
|
|
57
|
-
if not
|
|
58
|
-
|
|
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")
|
|
59
92
|
return
|
|
60
93
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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}")
|
|
103
144
|
|
|
104
145
|
except Exception as e:
|
|
105
|
-
eprint(f"[session_end] ERROR: {e}")
|
|
106
146
|
import traceback
|
|
107
|
-
|
|
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)
|
|
108
150
|
|
|
109
151
|
|
|
110
152
|
if __name__ == "__main__":
|
|
111
|
-
|
|
153
|
+
from lib.base.hook_utils import run_hook
|
|
154
|
+
run_hook(main, "session_end")
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""SessionStart hook for
|
|
2
|
+
"""SessionStart hook for post-compaction and post-clear restore.
|
|
3
3
|
|
|
4
4
|
This hook fires when a new session starts. It handles:
|
|
5
5
|
|
|
6
|
-
1.
|
|
7
|
-
|
|
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.
|
|
8
11
|
|
|
9
|
-
2. Post-compaction restore
|
|
10
|
-
|
|
11
|
-
|
|
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.
|
|
12
15
|
|
|
13
16
|
Hook input:
|
|
14
17
|
{
|
|
@@ -20,7 +23,6 @@ Hook input:
|
|
|
20
23
|
...
|
|
21
24
|
}
|
|
22
25
|
"""
|
|
23
|
-
import json
|
|
24
26
|
import sys
|
|
25
27
|
from pathlib import Path
|
|
26
28
|
|
|
@@ -29,48 +31,10 @@ SCRIPT_DIR = Path(__file__).resolve().parent
|
|
|
29
31
|
SHARED_LIB = SCRIPT_DIR.parent / "lib"
|
|
30
32
|
sys.path.insert(0, str(SHARED_LIB.parent))
|
|
31
33
|
|
|
32
|
-
from lib.base.hook_utils import load_hook_input
|
|
33
|
-
from lib.base.utils import
|
|
34
|
-
from lib.context.
|
|
35
|
-
|
|
36
|
-
get_context_by_session_id,
|
|
37
|
-
update_plan_status,
|
|
38
|
-
update_context_session_id,
|
|
39
|
-
)
|
|
40
|
-
from lib.context.auto_state import load_auto_state
|
|
41
|
-
from lib.context.discovery import (
|
|
42
|
-
_build_restore_sections,
|
|
43
|
-
find_plan_path,
|
|
44
|
-
format_relative_time,
|
|
45
|
-
)
|
|
46
|
-
from lib.context.task_sync import generate_task_summary
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _handle_clear_transition(hook_input, session_id, project_root):
|
|
50
|
-
"""Handle /clear mode transitions (existing behavior)."""
|
|
51
|
-
permission_mode = hook_input.get("permission_mode", "default")
|
|
52
|
-
|
|
53
|
-
if permission_mode == "plan":
|
|
54
|
-
eprint("[session_start] Skipping: permission_mode is 'plan' (in planning mode)")
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
in_flight_contexts = get_all_in_flight_contexts(project_root)
|
|
58
|
-
if not in_flight_contexts:
|
|
59
|
-
eprint("[session_start] No in-flight contexts found")
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
pending_contexts = [
|
|
63
|
-
ctx for ctx in in_flight_contexts
|
|
64
|
-
if ctx.in_flight and ctx.in_flight.mode == "pending_implementation"
|
|
65
|
-
]
|
|
66
|
-
for ctx in pending_contexts:
|
|
67
|
-
eprint(f"[session_start] Transitioning {ctx.id} from pending_implementation to implementing")
|
|
68
|
-
update_plan_status(ctx.id, "implementing", project_root=project_root)
|
|
69
|
-
update_context_session_id(ctx.id, session_id, project_root)
|
|
70
|
-
eprint(f"[session_start] Bound session {session_id[:8]}... to context {ctx.id}")
|
|
71
|
-
|
|
72
|
-
if pending_contexts:
|
|
73
|
-
eprint(f"[session_start] Transitioned {len(pending_contexts)} context(s) to implementing")
|
|
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
|
|
74
38
|
|
|
75
39
|
|
|
76
40
|
def _handle_compact_restore(hook_input, session_id, project_root):
|
|
@@ -78,30 +42,28 @@ def _handle_compact_restore(hook_input, session_id, project_root):
|
|
|
78
42
|
Handle post-compaction restore.
|
|
79
43
|
|
|
80
44
|
After compaction, the session is already bound to a context.
|
|
81
|
-
Load
|
|
45
|
+
Load state and inject rich restoration context via additionalContext.
|
|
82
46
|
"""
|
|
83
|
-
|
|
84
|
-
if not
|
|
85
|
-
|
|
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")
|
|
86
50
|
return
|
|
87
51
|
|
|
88
|
-
|
|
89
|
-
eprint(f"[session_start] Post-compaction restore for context: {context_id}")
|
|
52
|
+
log_info("session_start", f"Post-compaction restore for context: {state.id}")
|
|
90
53
|
|
|
91
54
|
# Build restoration context
|
|
92
|
-
mode_display = "Active"
|
|
93
|
-
if context.in_flight and context.in_flight.mode != "none":
|
|
94
|
-
mode_display = context.in_flight.mode.replace("_", " ").title()
|
|
55
|
+
mode_display = state.mode.replace("_", " ").title() if state.mode != "idle" else "Active"
|
|
95
56
|
|
|
96
57
|
lines = [
|
|
97
|
-
f"## Resuming Context After Compaction: {
|
|
58
|
+
f"## Resuming Context After Compaction: {state.id}",
|
|
98
59
|
"",
|
|
99
|
-
f"**Summary:** {
|
|
60
|
+
f"**Summary:** {state.summary}",
|
|
100
61
|
f"**Mode:** {mode_display}",
|
|
101
62
|
]
|
|
102
63
|
|
|
103
|
-
# Add restore sections (
|
|
104
|
-
|
|
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)
|
|
105
67
|
if restore:
|
|
106
68
|
lines.append(restore)
|
|
107
69
|
|
|
@@ -116,21 +78,78 @@ def _handle_compact_restore(hook_input, session_id, project_root):
|
|
|
116
78
|
])
|
|
117
79
|
|
|
118
80
|
restore_context = "\n".join(lines)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
"additionalContext": restore_context
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
print(json.dumps(output, ensure_ascii=False))
|
|
127
|
-
eprint(f"[session_start] Injected post-compaction restore context for {context_id}")
|
|
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})
|
|
128
85
|
|
|
129
86
|
|
|
130
|
-
def
|
|
87
|
+
def _handle_clear_restore(hook_input, session_id, project_root):
|
|
131
88
|
"""
|
|
132
|
-
Handle
|
|
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.
|
|
94
|
+
|
|
95
|
+
Fix: find the has_plan context (set by session_end moments ago),
|
|
96
|
+
bind the new session to it, and inject restoration context.
|
|
133
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."""
|
|
134
153
|
try:
|
|
135
154
|
hook_input = load_hook_input()
|
|
136
155
|
if not hook_input:
|
|
@@ -141,20 +160,30 @@ def main():
|
|
|
141
160
|
session_id = hook_input.get("session_id", "unknown")
|
|
142
161
|
project_root = project_dir(hook_input)
|
|
143
162
|
|
|
144
|
-
|
|
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})
|
|
145
166
|
|
|
146
|
-
if source == "
|
|
147
|
-
|
|
148
|
-
|
|
167
|
+
if source == "compact":
|
|
168
|
+
log_diagnostic("session_start", "decide", "Taking compact restore path",
|
|
169
|
+
decision="compact_restore", reasoning="source=compact")
|
|
149
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)
|
|
150
175
|
else:
|
|
151
|
-
|
|
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}'")
|
|
152
179
|
|
|
153
180
|
except Exception as e:
|
|
154
|
-
eprint(f"[session_start] ERROR: {e}")
|
|
155
181
|
import traceback
|
|
156
|
-
|
|
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)
|
|
157
185
|
|
|
158
186
|
|
|
159
187
|
if __name__ == "__main__":
|
|
160
|
-
|
|
188
|
+
from lib.base.hook_utils import run_hook
|
|
189
|
+
run_hook(main, "session_start")
|