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.
- package/bin/run.js +5 -2
- package/dist/lib/claude-settings-types.d.ts +2 -0
- package/dist/templates/CLAUDE.md +49 -18
- 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_atomicity.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 +128 -194
- package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
- package/dist/templates/_shared/hooks/pre_compact.py +104 -0
- package/dist/templates/_shared/hooks/session_end.py +154 -0
- package/dist/templates/_shared/hooks/session_start.py +145 -59
- package/dist/templates/_shared/hooks/task_create_capture.py +26 -49
- package/dist/templates/_shared/hooks/task_update_capture.py +42 -100
- package/dist/templates/_shared/hooks/user_prompt_submit.py +63 -77
- 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__/constants.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/constants.py +18 -4
- 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 +49 -11
- 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 +25 -79
- 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 +64 -9
- 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/agents/CLAUDE.md +1 -1
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +57 -22
- 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 -57
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +208 -158
- 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 +35 -10
- 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 +103 -42
- 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 +210 -43
- 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 -205
- 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 -1054
- package/dist/templates/_shared/lib/context/discovery.py +0 -444
- package/dist/templates/_shared/lib/context/event_log.py +0 -308
- package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
- package/dist/templates/_shared/lib/context/task_sync.py +0 -290
- 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
|
@@ -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
|
|
2
|
+
"""SessionStart hook for post-compaction and post-clear restore.
|
|
3
3
|
|
|
4
|
-
This hook fires when a new session starts. It handles
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
|
37
|
-
from lib.context.
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
43
|
-
from lib.context.
|
|
44
|
-
from lib.context.
|
|
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
|
-
|
|
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
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
107
|
-
|
|
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
|
-
|
|
95
|
+
session_id=session_id,
|
|
96
|
+
project_root=project_root,
|
|
119
97
|
)
|
|
120
98
|
|
|
121
|
-
if
|
|
122
|
-
|
|
99
|
+
if task:
|
|
100
|
+
log_info("task_create_capture", f"Recorded task: {task['id']} in {context_id}")
|
|
123
101
|
else:
|
|
124
|
-
|
|
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")
|