aiwcli 0.9.7 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/templates/CLAUDE.md +49 -18
  2. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  3. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  4. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  5. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  6. package/dist/templates/_shared/hooks/context_enforcer.py +4 -4
  7. package/dist/templates/_shared/hooks/context_monitor.py +78 -1
  8. package/dist/templates/_shared/hooks/pre_compact.py +89 -0
  9. package/dist/templates/_shared/hooks/session_end.py +111 -0
  10. package/dist/templates/_shared/hooks/session_start.py +104 -47
  11. package/dist/templates/_shared/hooks/task_create_atomicity.py +33 -61
  12. package/dist/templates/_shared/hooks/task_create_capture.py +1 -0
  13. package/dist/templates/_shared/hooks/task_update_capture.py +15 -0
  14. package/dist/templates/_shared/hooks/user_prompt_submit.py +13 -27
  15. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  17. package/dist/templates/_shared/lib/base/constants.py +18 -4
  18. package/dist/templates/_shared/lib/base/utils.py +9 -4
  19. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  20. package/dist/templates/_shared/lib/context/auto_state.py +167 -0
  21. package/dist/templates/_shared/lib/context/context_manager.py +6 -3
  22. package/dist/templates/_shared/lib/context/discovery.py +167 -57
  23. package/dist/templates/_shared/lib/context/event_log.py +8 -0
  24. package/dist/templates/_shared/lib/context/task_sync.py +160 -43
  25. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  26. package/dist/templates/_shared/lib/templates/plan_context.py +24 -41
  27. package/dist/templates/cc-native/.claude/settings.json +23 -1
  28. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -1
  29. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +8 -1
  30. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  31. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  32. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -2
  33. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +65 -47
  34. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +29 -6
  35. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  36. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  37. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +71 -15
  38. package/dist/templates/cc-native/_cc-native/lib/utils.py +3 -3
  39. package/oclif.manifest.json +1 -1
  40. package/package.json +1 -1
@@ -17,24 +17,53 @@ Include `_output/{method}/` in template `.gitignore`.
17
17
 
18
18
  ## Directory Structure
19
19
 
20
+ Each template installs into `.aiwcli/` (method files) and `.{ide}/` (IDE integration). The `_shared/` template provides cross-method infrastructure used by all methods.
21
+
20
22
  ```
21
- packages/cli/src/templates/{method}/
22
- ├── _{method}/ # Method-specific shared files
23
- │ ├── templates/*.md.template
24
- │ └── workflows/*.md # Canonical workflow definitions
25
- ├── .{ide}/{ide-folder}/{method}/*.md # IDE command stubs
26
- ├── .gitignore # Output ignore rules
27
- ├── {METHOD}-README.md # User documentation
28
- ├── TEMPLATE-SCHEMA.md # Schema reference
29
- └── MIGRATION.md # Breaking changes
23
+ packages/cli/src/templates/
24
+ ├── _shared/ # Cross-method infrastructure (installed by all methods)
25
+ │ ├── hooks/ # Shared hook scripts (context, tasks, sessions)
26
+ │ └── lib/ # Shared Python libraries
27
+ ├── base/ # Core: atomic_write, constants, inference, utils
28
+ ├── context/ # Context management, event sourcing, discovery
29
+ ├── handoff/ # Session handoff document generation
30
+ │ └── templates/ # Output formatters, plan context templates
31
+
32
+ ├── cc-native/ # CC-Native method template
33
+ │ ├── _cc-native/ # Method-specific hooks, lib, agents, workflows, scripts
34
+ │ ├── _shared/ # Copy of shared infrastructure (installed together)
35
+ │ ├── .claude/ # Claude Code: settings.json, commands/, agents/
36
+ │ ├── .windsurf/ # Windsurf: workflows/
37
+ │ └── .gitignore
38
+
39
+ ├── gsd/ # GSD method template
40
+ │ ├── .aiwcli/_gsd/ # Templates, workflows, hooks, config, docs
41
+ │ ├── .claude/ # Claude Code: settings.json, commands/, agents/
42
+ │ ├── .windsurf/ # Windsurf: workflows/
43
+ │ ├── GSD-README.md
44
+ │ ├── TEMPLATE-SCHEMA.md
45
+ │ └── MIGRATION.md
46
+
47
+ ├── bmad/ # BMAD method template
48
+ │ ├── .aiwcli/_bmad/ # Agents, workflows, teams, testarch, config
49
+ │ ├── .claude/ # Claude Code: settings.json, commands/
50
+ │ └── ...
51
+
52
+ ├── planning-with-files/ # Planning-with-Files method template
53
+ │ ├── .claude/ # Claude Code: settings.json, skills/
54
+ │ ├── .windsurf/ # Windsurf: workflows/, scripts/
55
+ │ └── ...
56
+
57
+ └── CLAUDE.md # This file
30
58
  ```
31
59
 
32
60
  ### Tier Details
33
61
 
34
62
  | Tier | Location | Purpose |
35
63
  |------|----------|---------|
36
- | General | `_{method}/` | IDE-agnostic templates and canonical workflows |
37
- | IDE | `.{ide}/{folder}/{method}/` | Lightweight stubs that load canonical workflows |
64
+ | Shared | `_shared/` | Cross-method hooks and libraries (context management, task sync, sessions) |
65
+ | Method | `_{method}/` or `.aiwcli/_{method}/` | Method-specific templates, workflows, hooks, config |
66
+ | IDE | `.{ide}/` | IDE-specific command stubs, settings, workflow definitions |
38
67
  | Config | `.{ide}/settings.json` | Hooks, model prefs, method settings (merged on install) |
39
68
 
40
69
  ---
@@ -57,7 +86,7 @@ When multiple templates install, settings.json files merge:
57
86
 
58
87
  ## Hooks
59
88
 
60
- **Location:** `.claude/hooks/{method}-{hook-name}.{ext}`
89
+ **Location:** Hooks live in `.aiwcli/_shared/hooks/` (cross-method) and `.aiwcli/_{method}/hooks/` (method-specific). They are configured in `.{ide}/settings.json`, not placed in IDE directories.
61
90
 
62
91
  **Configuration:**
63
92
  ```json
@@ -65,14 +94,14 @@ When multiple templates install, settings.json files merge:
65
94
  "hooks": {
66
95
  "PostToolUse": [{
67
96
  "matcher": "Write",
68
- "hooks": [{ "type": "command", "command": "python .claude/hooks/gsd-plan-review.py", "timeout": 300000 }]
97
+ "hooks": [{ "type": "command", "command": "python .aiwcli/_cc-native/hooks/cc-native-plan-review.py", "timeout": 300000 }]
69
98
  }]
70
99
  }
71
100
  }
72
101
  ```
73
102
 
74
103
  **Requirements:**
75
- - Prefix with method name (e.g., `gsd-plan-review.py`)
104
+ - Prefix method-specific hooks with method name (e.g., `cc-native-plan-review.py`)
76
105
  - Use relative paths from project root
77
106
  - Write outputs to `_output/{method}/`
78
107
  - Specify timeouts
@@ -119,8 +148,8 @@ Load and execute `_{method}/workflows/{name}.md`.
119
148
  | Reference Type | Pattern |
120
149
  |----------------|---------|
121
150
  | Templates | `_{method}/templates/FILE.md.template` |
122
- | Workflows (Claude) | `/gsd:other-workflow` |
123
- | Workflows (Windsurf) | `other-workflow` from GSD workflows |
151
+ | Workflows (Claude) | `/gsd:workflow-name` (maps to `.claude/commands/gsd/workflow-name.md`) |
152
+ | Workflows (Windsurf) | `workflow-name` from method workflows |
124
153
  | Outputs | `_output/{method}/{subdir}/FILE.md` |
125
154
 
126
155
  ---
@@ -143,8 +172,8 @@ Load and execute `_{method}/workflows/{name}.md`.
143
172
 
144
173
  **New Template:**
145
174
  - [ ] Create `_{method}/` with `templates/` and `workflows/`
146
- - [ ] Create `.claude/commands/{method}/` stubs
147
- - [ ] Create `.windsurf/workflows/{method}/` stubs
175
+ - [ ] Create `.claude/commands/{method}/` stubs (Claude Code)
176
+ - [ ] Create `.windsurf/workflows/{method}/` stubs (Windsurf)
148
177
  - [ ] Add `.gitignore` with `_output/{method}/`
149
178
  - [ ] Create `{METHOD}-README.md`, `TEMPLATE-SCHEMA.md`, `MIGRATION.md`
150
179
  - [ ] Configure method-namespaced settings in `.claude/settings.json`
@@ -165,6 +194,7 @@ Load and execute `_{method}/workflows/{name}.md`.
165
194
  - Keep canonical workflows in `_{method}/workflows/`
166
195
  - Use relative paths from project root
167
196
  - Document changes in TEMPLATE-SCHEMA.md
197
+ - Place hooks in `.aiwcli/` directories, wire them in `.{ide}/settings.json`
168
198
 
169
199
  **Avoid:**
170
200
  - Outputs in project root
@@ -172,3 +202,4 @@ Load and execute `_{method}/workflows/{name}.md`.
172
202
  - Hooks without method prefix
173
203
  - Full workflows in IDE command files
174
204
  - Hardcoded paths without method namespace
205
+ - Putting hook scripts directly in IDE directories (`.claude/hooks/`)
@@ -373,7 +373,7 @@ def determine_context(
373
373
  return (
374
374
  session_context.id,
375
375
  "session_match",
376
- format_active_context_reminder(session_context)
376
+ format_active_context_reminder(session_context, project_root)
377
377
  )
378
378
 
379
379
  # 2. Check for bare "^" - show context picker
@@ -433,11 +433,11 @@ def determine_context(
433
433
 
434
434
  # Use mode-specific formatter for better continuation context
435
435
  if mode == "pending_implementation":
436
- output = format_pending_plan_continuation(ctx)
436
+ output = format_pending_plan_continuation(ctx, project_root)
437
437
  elif mode == "implementing":
438
- output = format_implementation_continuation(ctx)
438
+ output = format_implementation_continuation(ctx, project_root)
439
439
  else:
440
- output = format_active_context_reminder(ctx)
440
+ output = format_active_context_reminder(ctx, project_root, include_restore=True)
441
441
 
442
442
  return (ctx.id, "auto_selected", output)
443
443
 
@@ -62,8 +62,15 @@ from lib.context.context_manager import (
62
62
  get_context_by_session_id,
63
63
  update_plan_status,
64
64
  )
65
+ from lib.context.auto_state import save_auto_state
66
+ from lib.context.event_log import EVENT_AUTO_STATE_SAVED, append_event
67
+
68
+ # Module-level flag: only save auto-state once per process lifetime
69
+ # Since hooks are separate processes per invocation, we use a file marker instead
70
+ _PROGRESSIVE_SAVE_MARKER = ".progressive-save-done"
65
71
 
66
72
  # Configuration
73
+ SAVE_STATE_THRESHOLD = 60 # Silently save auto-state at 60% remaining
67
74
  LOW_CONTEXT_THRESHOLD = 40 # Warn when below 40% remaining
68
75
  CRITICAL_CONTEXT_THRESHOLD = 25 # Urgent warning below 25%
69
76
 
@@ -242,6 +249,71 @@ def check_and_transition_mode(hook_input: dict) -> None:
242
249
  update_plan_status(context.id, "implementing", project_root=project_root)
243
250
 
244
251
 
252
+ def _try_progressive_save(hook_input: dict, percent_remaining: int) -> None:
253
+ """
254
+ Silently save auto-state at SAVE_STATE_THRESHOLD (60%).
255
+
256
+ Uses a marker file in the context folder to ensure this fires only
257
+ once per session. The marker is the session_id written to a file.
258
+
259
+ Args:
260
+ hook_input: Hook input data from Claude Code
261
+ percent_remaining: Current context percentage remaining
262
+ """
263
+ try:
264
+ session_id = hook_input.get("session_id", "")
265
+ if not session_id:
266
+ return
267
+
268
+ project_root = project_dir(hook_input)
269
+ context = get_context_by_session_id(session_id, project_root)
270
+ if not context:
271
+ return
272
+
273
+ from lib.base.constants import get_context_dir
274
+ marker_path = get_context_dir(context.id, project_root) / _PROGRESSIVE_SAVE_MARKER
275
+ # Check if already saved for this session
276
+ if marker_path.exists():
277
+ try:
278
+ saved_session = marker_path.read_text(encoding="utf-8").strip()
279
+ if saved_session == session_id:
280
+ return # Already saved this session
281
+ except OSError:
282
+ pass
283
+
284
+ eprint(f"[context_monitor] Progressive save at {percent_remaining}% remaining")
285
+
286
+ in_flight_mode = context.in_flight.mode if context.in_flight else "none"
287
+ plan_path = context.in_flight.artifact_path if context.in_flight else None
288
+ handoff_path = context.in_flight.handoff_path if context.in_flight else None
289
+ transcript_path = hook_input.get("transcript_path")
290
+
291
+ saved = save_auto_state(
292
+ context_id=context.id,
293
+ session_id=session_id,
294
+ save_reason="progressive",
295
+ project_root=project_root,
296
+ in_flight_mode=in_flight_mode,
297
+ plan_path=plan_path,
298
+ handoff_path=handoff_path,
299
+ transcript_path=transcript_path,
300
+ )
301
+
302
+ if saved:
303
+ append_event(
304
+ context.id, EVENT_AUTO_STATE_SAVED, project_root,
305
+ session_id=session_id, save_reason="progressive",
306
+ )
307
+ # Write marker so we don't save again this session
308
+ try:
309
+ marker_path.write_text(session_id, encoding="utf-8")
310
+ except OSError:
311
+ pass
312
+
313
+ except Exception as e:
314
+ eprint(f"[context_monitor] Progressive save error (non-fatal): {e}")
315
+
316
+
245
317
  def check_context_level(hook_input: dict) -> Optional[str]:
246
318
  """
247
319
  Check context level and return warning if low.
@@ -270,7 +342,13 @@ def check_context_level(hook_input: dict) -> Optional[str]:
270
342
  percent_remaining = max(0, min(100, int((remaining / max_tokens) * 100)))
271
343
 
272
344
  # 4. Most common case: context is fine, exit early
345
+ if percent_remaining > SAVE_STATE_THRESHOLD:
346
+ return None
347
+
348
+ # === PROGRESSIVE SAVE: At 60% remaining, silently save auto-state ===
273
349
  if percent_remaining > LOW_CONTEXT_THRESHOLD:
350
+ # Only save once per session (check marker file)
351
+ _try_progressive_save(hook_input, percent_remaining)
274
352
  return None
275
353
 
276
354
  # === SLOW PATH: Only reached when context is low (rare) ===
@@ -320,7 +398,6 @@ def main():
320
398
  # Plain stdout from PostToolUse only goes to verbose mode, not Claude's context
321
399
  output = {
322
400
  "hookSpecificOutput": {
323
- "hookEventName": "PostToolUse",
324
401
  "additionalContext": warning
325
402
  }
326
403
  }
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env python3
2
+ """PreCompact hook - saves auto-state before context compaction.
3
+
4
+ Critical: saves state before context compaction destroys token history.
5
+ After compaction, SessionStart fires with source="compact" and the
6
+ restored auto-state provides continuity context.
7
+
8
+ Hook input (from Claude Code):
9
+ {
10
+ "hook_event_name": "PreCompact",
11
+ "session_id": "abc123",
12
+ "transcript_path": "/path/to/transcript.jsonl",
13
+ "cwd": "/path/to/project",
14
+ ...
15
+ }
16
+
17
+ Hook output:
18
+ - Silent (no stdout output needed)
19
+ - Logs to stderr for debugging
20
+ """
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ # Add parent directories to path for imports
25
+ SCRIPT_DIR = Path(__file__).resolve().parent
26
+ SHARED_LIB = SCRIPT_DIR.parent / "lib"
27
+ sys.path.insert(0, str(SHARED_LIB.parent))
28
+
29
+ from lib.base.hook_utils import load_hook_input
30
+ from lib.base.utils import eprint, project_dir
31
+ from lib.context.context_manager import get_context_by_session_id
32
+ from lib.context.event_log import EVENT_AUTO_STATE_SAVED, append_event
33
+ from lib.context.auto_state import save_auto_state
34
+
35
+
36
+ def main():
37
+ """Save auto-state before compaction."""
38
+ try:
39
+ hook_input = load_hook_input()
40
+ if not hook_input:
41
+ return
42
+
43
+ session_id = hook_input.get("session_id", "")
44
+ transcript_path = hook_input.get("transcript_path")
45
+ project_root = project_dir(hook_input)
46
+
47
+ if not session_id:
48
+ eprint("[pre_compact] No session_id, skipping")
49
+ return
50
+
51
+ eprint(f"[pre_compact] Saving state before compaction: {session_id[:8]}...")
52
+
53
+ # Find context bound to this session
54
+ context = get_context_by_session_id(session_id, project_root)
55
+ if not context:
56
+ eprint("[pre_compact] No context bound to this session, skipping")
57
+ return
58
+
59
+ context_id = context.id
60
+ in_flight_mode = context.in_flight.mode if context.in_flight else "none"
61
+ plan_path = context.in_flight.artifact_path if context.in_flight else None
62
+ handoff_path = context.in_flight.handoff_path if context.in_flight else None
63
+
64
+ saved = save_auto_state(
65
+ context_id=context_id,
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",
79
+ )
80
+ eprint(f"[pre_compact] Auto-state saved for {context_id}")
81
+
82
+ except Exception as e:
83
+ eprint(f"[pre_compact] ERROR: {e}")
84
+ import traceback
85
+ eprint(traceback.format_exc())
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env python3
2
+ """SessionEnd hook - records session boundary and saves auto-state.
3
+
4
+ Fires when session terminates (quit, /clear, logout). Creates a session
5
+ boundary marker in events.jsonl and writes auto-state.json for restoration.
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 sys
22
+ from pathlib import Path
23
+
24
+ # Add parent directories to path for imports
25
+ SCRIPT_DIR = Path(__file__).resolve().parent
26
+ SHARED_LIB = SCRIPT_DIR.parent / "lib"
27
+ sys.path.insert(0, str(SHARED_LIB.parent))
28
+
29
+ from lib.base.hook_utils import load_hook_input
30
+ from lib.base.utils import eprint, project_dir
31
+ from lib.context.context_manager import get_context_by_session_id
32
+ from lib.context.event_log import get_current_state, EVENT_AUTO_STATE_SAVED, append_event
33
+ from lib.context.task_sync import record_session_ended
34
+ from lib.context.auto_state import save_auto_state
35
+
36
+
37
+ def main():
38
+ """Record session boundary and save auto-state."""
39
+ try:
40
+ hook_input = load_hook_input()
41
+ if not hook_input:
42
+ return
43
+
44
+ session_id = hook_input.get("session_id", "")
45
+ source = hook_input.get("source", "other")
46
+ transcript_path = hook_input.get("transcript_path")
47
+ project_root = project_dir(hook_input)
48
+
49
+ if not session_id:
50
+ eprint("[session_end] No session_id, skipping")
51
+ return
52
+
53
+ eprint(f"[session_end] Session ending: {session_id[:8]}... reason={source}")
54
+
55
+ # Find context bound to this session
56
+ context = get_context_by_session_id(session_id, project_root)
57
+ if not context:
58
+ eprint("[session_end] No context bound to this session, skipping")
59
+ return
60
+
61
+ context_id = context.id
62
+ eprint(f"[session_end] Found context: {context_id}")
63
+
64
+ # Get current task state for the session boundary marker
65
+ state = get_current_state(context_id, project_root)
66
+ active_tasks = [t.id for t in state.tasks if t.status == "in_progress"]
67
+ pending_tasks = [t.id for t in state.tasks if t.status == "pending"]
68
+
69
+ # Record session_ended event in events.jsonl
70
+ record_session_ended(
71
+ context_id=context_id,
72
+ session_id=session_id,
73
+ reason=source,
74
+ active_tasks=active_tasks,
75
+ pending_tasks=pending_tasks,
76
+ project_root=project_root,
77
+ )
78
+ eprint(f"[session_end] Recorded session_ended: active={len(active_tasks)}, pending={len(pending_tasks)}")
79
+
80
+ # Save auto-state.json
81
+ in_flight_mode = context.in_flight.mode if context.in_flight else "none"
82
+ plan_path = context.in_flight.artifact_path if context.in_flight else None
83
+ handoff_path = context.in_flight.handoff_path if context.in_flight else None
84
+
85
+ saved = save_auto_state(
86
+ context_id=context_id,
87
+ session_id=session_id,
88
+ save_reason=source,
89
+ project_root=project_root,
90
+ in_flight_mode=in_flight_mode,
91
+ plan_path=plan_path,
92
+ handoff_path=handoff_path,
93
+ transcript_path=transcript_path,
94
+ )
95
+
96
+ if saved:
97
+ # Record auto_state_saved event
98
+ append_event(
99
+ context_id, EVENT_AUTO_STATE_SAVED, project_root,
100
+ session_id=session_id, save_reason=source,
101
+ )
102
+ eprint(f"[session_end] Auto-state saved for {context_id}")
103
+
104
+ except Exception as e:
105
+ eprint(f"[session_end] ERROR: {e}")
106
+ import traceback
107
+ eprint(traceback.format_exc())
108
+
109
+
110
+ if __name__ == "__main__":
111
+ main()
@@ -1,18 +1,14 @@
1
1
  #!/usr/bin/env python3
2
- """SessionStart hook for mode transitions after /clear.
2
+ """SessionStart hook for mode transitions and post-compaction restore.
3
3
 
4
- This hook fires when a new session starts. It handles the critical transition
5
- from `pending_implementation` to `implementing` when a session starts after
6
- /clear with bypass permissions.
4
+ This hook fires when a new session starts. It handles:
7
5
 
8
- The flow is:
9
- 1. User approves plan (ExitPlanMode) -> mode = pending_implementation
10
- 2. User clicks "yes and clear and bypass permissions"
11
- 3. SessionStart fires with source="clear" and permission_mode="bypassPermissions"
12
- 4. This hook transitions mode to "implementing"
6
+ 1. Mode transition from `pending_implementation` to `implementing` when
7
+ a session starts after /clear with bypass permissions.
13
8
 
14
- Without this hook, the mode stays stuck at pending_implementation because
15
- UserPromptSubmit may not receive the correct permission_mode after /clear.
9
+ 2. Post-compaction restore: when source="compact", the session is already
10
+ bound to a context. Load auto-state and inject rich restoration context
11
+ so Claude can continue seamlessly after compaction.
16
12
 
17
13
  Hook input:
18
14
  {
@@ -24,6 +20,7 @@ Hook input:
24
20
  ...
25
21
  }
26
22
  """
23
+ import json
27
24
  import sys
28
25
  from pathlib import Path
29
26
 
@@ -36,22 +33,106 @@ from lib.base.hook_utils import load_hook_input
36
33
  from lib.base.utils import eprint, project_dir
37
34
  from lib.context.context_manager import (
38
35
  get_all_in_flight_contexts,
36
+ get_context_by_session_id,
39
37
  update_plan_status,
40
38
  update_context_session_id,
41
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
42
47
 
43
48
 
44
- def main():
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")
74
+
75
+
76
+ def _handle_compact_restore(hook_input, session_id, project_root):
77
+ """
78
+ Handle post-compaction restore.
79
+
80
+ After compaction, the session is already bound to a context.
81
+ Load auto-state and inject rich restoration context via additionalContext.
45
82
  """
46
- Handle mode transitions on session start.
83
+ context = get_context_by_session_id(session_id, project_root)
84
+ if not context:
85
+ eprint("[session_start] No context bound to session after compact")
86
+ return
87
+
88
+ context_id = context.id
89
+ eprint(f"[session_start] Post-compaction restore for context: {context_id}")
90
+
91
+ # 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()
95
+
96
+ lines = [
97
+ f"## Resuming Context After Compaction: {context_id}",
98
+ "",
99
+ f"**Summary:** {context.summary}",
100
+ f"**Mode:** {mode_display}",
101
+ ]
102
+
103
+ # Add restore sections (auto-state, tasks, git)
104
+ restore = _build_restore_sections(context, project_root)
105
+ if restore:
106
+ lines.append(restore)
107
+
108
+ lines.extend([
109
+ "",
110
+ "---",
111
+ "",
112
+ "**Instructions:**",
113
+ "Context was compacted to free memory. Your previous conversation has been summarized.",
114
+ "1. Review the previous work above",
115
+ "2. Continue from where you left off",
116
+ ])
117
+
118
+ restore_context = "\n".join(lines)
119
+
120
+ # Output as additionalContext so Claude sees it
121
+ output = {
122
+ "hookSpecificOutput": {
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}")
128
+
47
129
 
48
- When source is "clear" and permission_mode is "bypassPermissions" or "acceptEdits",
49
- transition any pending_implementation context to implementing.
130
+ def main():
131
+ """
132
+ Handle mode transitions and post-compaction restore on session start.
50
133
  """
51
134
  try:
52
- # Read hook input using shared utility
53
135
  hook_input = load_hook_input()
54
-
55
136
  if not hook_input:
56
137
  return
57
138
 
@@ -62,36 +143,12 @@ def main():
62
143
 
63
144
  eprint(f"[session_start] source={source}, permission_mode={permission_mode}, session={session_id[:8]}...")
64
145
 
65
- # Only handle /clear with bypass/accept permissions
66
- if source != "clear":
67
- eprint(f"[session_start] Skipping: source is '{source}', not 'clear'")
68
- return
69
-
70
- if permission_mode == "plan":
71
- eprint(f"[session_start] Skipping: permission_mode is 'plan' (in planning mode)")
72
- return
73
-
74
- # Find contexts in pending_implementation mode
75
- in_flight_contexts = get_all_in_flight_contexts(project_root)
76
- pending_contexts = [
77
- ctx for ctx in in_flight_contexts
78
- if ctx.in_flight and ctx.in_flight.mode == "pending_implementation"
79
- ]
80
-
81
- if not pending_contexts:
82
- eprint("[session_start] No pending_implementation contexts found")
83
- return
84
-
85
- # Transition each pending context to implementing
86
- for ctx in pending_contexts:
87
- eprint(f"[session_start] Transitioning {ctx.id} from pending_implementation to implementing")
88
- update_plan_status(ctx.id, "implementing", project_root=project_root)
89
-
90
- # Also bind this session to the context
91
- update_context_session_id(ctx.id, session_id, project_root)
92
- eprint(f"[session_start] Bound session {session_id[:8]}... to context {ctx.id}")
93
-
94
- eprint(f"[session_start] Transitioned {len(pending_contexts)} context(s) to implementing")
146
+ if source == "clear":
147
+ _handle_clear_transition(hook_input, session_id, project_root)
148
+ elif source == "compact":
149
+ _handle_compact_restore(hook_input, session_id, project_root)
150
+ else:
151
+ eprint(f"[session_start] No action for source='{source}'")
95
152
 
96
153
  except Exception as e:
97
154
  eprint(f"[session_start] ERROR: {e}")