aiwcli 0.9.8 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +114 -60
- package/dist/templates/_shared/hooks/session_start.py +127 -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 +47 -81
- 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 +207 -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 +317 -0
- package/dist/templates/_shared/lib/context/context_selector.py +508 -0
- package/dist/templates/_shared/lib/context/context_store.py +653 -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 +22 -37
- 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 +31 -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 +37 -14
- 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 +54 -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 +76 -89
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +163 -131
- 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,88 @@ 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
|
-
|
|
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
|
+
state.plan_consumed = False
|
|
125
|
+
log_info("session_end", f"Fallback: assigned archived plan for {state.id} (hash: {state.plan_hash})")
|
|
126
|
+
except Exception as e:
|
|
127
|
+
log_warn("session_end", f"Fallback plan assignment failed: {e}")
|
|
128
|
+
|
|
129
|
+
# If a plan is assigned, not yet consumed, and mode is active, stage it for next session
|
|
130
|
+
if state.plan_hash and state.mode == "active" and not state.plan_consumed:
|
|
131
|
+
state.mode = "has_plan"
|
|
132
|
+
log_info("session_end", f"Staged plan for next session: {state.id} -> has_plan")
|
|
133
|
+
elif state.plan_hash and state.mode == "active" and state.plan_consumed:
|
|
134
|
+
log_debug("session_end", f"Plan already consumed for {state.id}, not re-staging")
|
|
135
|
+
log_diagnostic("session_end", "decide", f"Skipping re-stage for {state.id}",
|
|
136
|
+
decision="skip_restage", reasoning="plan_hash exists but plan_consumed=True",
|
|
137
|
+
inputs={"plan_hash": state.plan_hash, "plan_consumed": True})
|
|
138
|
+
|
|
139
|
+
# Handoff staging (mirrors plan staging above)
|
|
140
|
+
# Note: if plan already set has_plan, mode != "active" so handoff check skips (plan takes priority)
|
|
141
|
+
if state.handoff_path and state.mode == "active" and not state.handoff_consumed:
|
|
142
|
+
state.mode = "has_handoff"
|
|
143
|
+
log_info("session_end", f"Staged handoff for next session: {state.id} -> has_handoff")
|
|
144
|
+
elif state.handoff_path and state.mode == "active" and state.handoff_consumed:
|
|
145
|
+
log_debug("session_end", f"Handoff already consumed for {state.id}, not re-staging")
|
|
146
|
+
|
|
147
|
+
if save_state(state, project_root):
|
|
148
|
+
log_info("session_end", f"Saved last_session for {state.id}")
|
|
149
|
+
log_diagnostic("session_end", "result", f"Saved state for {state.id}",
|
|
150
|
+
decision="saved", inputs={"context_id": state.id, "mode": state.mode,
|
|
151
|
+
"has_plan_hash": bool(state.plan_hash),
|
|
152
|
+
"git_files": len(git_state.get("uncommitted_files", []))})
|
|
153
|
+
else:
|
|
154
|
+
log_error("session_end", f"Failed to save state for {state.id}")
|
|
103
155
|
|
|
104
156
|
except Exception as e:
|
|
105
|
-
eprint(f"[session_end] ERROR: {e}")
|
|
106
157
|
import traceback
|
|
107
|
-
|
|
158
|
+
tb = traceback.format_exc()
|
|
159
|
+
from lib.base.hook_utils import log_hook_error
|
|
160
|
+
log_hook_error("session_end", e, "SessionEnd", traceback_str=tb)
|
|
108
161
|
|
|
109
162
|
|
|
110
163
|
if __name__ == "__main__":
|
|
111
|
-
|
|
164
|
+
from lib.base.hook_utils import run_hook
|
|
165
|
+
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, format_handoff_continuation
|
|
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,95 @@ 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
|
+
# Check for has_handoff contexts (mirrors plan logic)
|
|
106
|
+
has_handoff = [
|
|
107
|
+
c for c in get_all_contexts(status="active", project_root=project_root)
|
|
108
|
+
if c.mode == "has_handoff"
|
|
109
|
+
]
|
|
110
|
+
if has_handoff:
|
|
111
|
+
target = has_handoff[0]
|
|
112
|
+
log_info("session_start", f"Found has_handoff context after /clear: {target.id}")
|
|
113
|
+
bind_session(target.id, session_id, project_root)
|
|
114
|
+
log_info("session_start", f"Bound session {session_id[:8]}... to {target.id}")
|
|
115
|
+
update_mode(target.id, "active", project_root=project_root, handoff_consumed=True)
|
|
116
|
+
log_info("session_start", f"Transitioned {target.id}: has_handoff -> active (handoff_consumed=True)")
|
|
117
|
+
restore_context = format_handoff_continuation(target, project_root)
|
|
118
|
+
emit_context(restore_context)
|
|
119
|
+
log_info("session_start", f"Injected handoff-restore context for {target.id}")
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
log_debug("session_start", "No has_plan or has_handoff contexts found after /clear")
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
# Pick the most recently active one (first in list, already sorted)
|
|
126
|
+
target = has_plan[0]
|
|
127
|
+
log_info("session_start", f"Found has_plan context after /clear: {target.id}")
|
|
128
|
+
|
|
129
|
+
# Bind new session to this context
|
|
130
|
+
bind_session(target.id, session_id, project_root)
|
|
131
|
+
log_info("session_start", f"Bound session {session_id[:8]}... to {target.id}")
|
|
132
|
+
|
|
133
|
+
# Transition has_plan → active (consume the transient state)
|
|
134
|
+
update_mode(target.id, "active", project_root=project_root, plan_consumed=True)
|
|
135
|
+
log_info("session_start", f"Transitioned {target.id}: has_plan -> active (plan_consumed=True)")
|
|
136
|
+
|
|
137
|
+
# Inject restoration context (tasks, git state, plan path reference)
|
|
138
|
+
# Plan CONTENT is not injected — Claude Code auto-pastes it after /clear
|
|
139
|
+
mode_display = "Active (Plan Restored)"
|
|
140
|
+
lines = [
|
|
141
|
+
f"## Resuming Context After Plan Clear: {target.id}",
|
|
142
|
+
"",
|
|
143
|
+
f"**Summary:** {target.summary}",
|
|
144
|
+
f"**Mode:** {mode_display}",
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
restore = _build_restore_sections(target, project_root)
|
|
148
|
+
if restore:
|
|
149
|
+
lines.append(restore)
|
|
150
|
+
|
|
151
|
+
lines.extend([
|
|
152
|
+
"",
|
|
153
|
+
"---",
|
|
154
|
+
"",
|
|
155
|
+
"**Instructions:**",
|
|
156
|
+
"Context was cleared for plan implementation. Your plan content has been pasted above.",
|
|
157
|
+
"1. Review the plan content above",
|
|
158
|
+
"2. Implement the plan step by step",
|
|
159
|
+
])
|
|
160
|
+
|
|
161
|
+
restore_context = "\n".join(lines)
|
|
162
|
+
emit_context(restore_context)
|
|
163
|
+
log_info("session_start", f"Injected clear-restore context for {target.id}")
|
|
164
|
+
log_diagnostic("session_start", "result", f"Clear restore complete for {target.id}",
|
|
165
|
+
decision="injected", inputs={"context_id": target.id, "mode_transition": "has_plan->active"})
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def main():
|
|
169
|
+
"""Handle post-compaction and post-clear restore on session start."""
|
|
134
170
|
try:
|
|
135
171
|
hook_input = load_hook_input()
|
|
136
172
|
if not hook_input:
|
|
@@ -141,20 +177,30 @@ def main():
|
|
|
141
177
|
session_id = hook_input.get("session_id", "unknown")
|
|
142
178
|
project_root = project_dir(hook_input)
|
|
143
179
|
|
|
144
|
-
|
|
180
|
+
log_info("session_start", f"source={source}, permission_mode={permission_mode}, session={session_id[:8]}...")
|
|
181
|
+
log_diagnostic("session_start", "receive", f"source={source}, session={session_id[:8]}",
|
|
182
|
+
inputs={"source": source, "session_id": session_id[:12], "permission_mode": permission_mode})
|
|
145
183
|
|
|
146
|
-
if source == "
|
|
147
|
-
|
|
148
|
-
|
|
184
|
+
if source == "compact":
|
|
185
|
+
log_diagnostic("session_start", "decide", "Taking compact restore path",
|
|
186
|
+
decision="compact_restore", reasoning="source=compact")
|
|
149
187
|
_handle_compact_restore(hook_input, session_id, project_root)
|
|
188
|
+
elif source == "clear":
|
|
189
|
+
log_diagnostic("session_start", "decide", "Taking clear restore path",
|
|
190
|
+
decision="clear_restore", reasoning="source=clear, looking for has_plan context")
|
|
191
|
+
_handle_clear_restore(hook_input, session_id, project_root)
|
|
150
192
|
else:
|
|
151
|
-
|
|
193
|
+
log_diagnostic("session_start", "decide", f"No action for source={source}",
|
|
194
|
+
decision="skip", reasoning=f"source={source} has no handler")
|
|
195
|
+
log_debug("session_start", f"No action for source='{source}'")
|
|
152
196
|
|
|
153
197
|
except Exception as e:
|
|
154
|
-
eprint(f"[session_start] ERROR: {e}")
|
|
155
198
|
import traceback
|
|
156
|
-
|
|
199
|
+
tb = traceback.format_exc()
|
|
200
|
+
from lib.base.hook_utils import log_hook_error
|
|
201
|
+
log_hook_error("session_start", e, "SessionStart", traceback_str=tb)
|
|
157
202
|
|
|
158
203
|
|
|
159
204
|
if __name__ == "__main__":
|
|
160
|
-
|
|
205
|
+
from lib.base.hook_utils import run_hook
|
|
206
|
+
run_hook(main, "session_start")
|