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.
- package/dist/templates/CLAUDE.md +49 -18
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.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__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/context_enforcer.py +4 -4
- package/dist/templates/_shared/hooks/context_monitor.py +78 -1
- package/dist/templates/_shared/hooks/pre_compact.py +89 -0
- package/dist/templates/_shared/hooks/session_end.py +111 -0
- package/dist/templates/_shared/hooks/session_start.py +104 -47
- package/dist/templates/_shared/hooks/task_create_atomicity.py +33 -61
- package/dist/templates/_shared/hooks/task_create_capture.py +1 -0
- package/dist/templates/_shared/hooks/task_update_capture.py +15 -0
- package/dist/templates/_shared/hooks/user_prompt_submit.py +13 -27
- package/dist/templates/_shared/lib/base/__pycache__/constants.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/utils.py +9 -4
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/auto_state.py +167 -0
- package/dist/templates/_shared/lib/context/context_manager.py +6 -3
- package/dist/templates/_shared/lib/context/discovery.py +167 -57
- package/dist/templates/_shared/lib/context/event_log.py +8 -0
- package/dist/templates/_shared/lib/context/task_sync.py +160 -43
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +24 -41
- package/dist/templates/cc-native/.claude/settings.json +23 -1
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -1
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +8 -1
- 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/add_plan_context.py +0 -2
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +65 -47
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +29 -6
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.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/orchestrator.py +71 -15
- package/dist/templates/cc-native/_cc-native/lib/utils.py +3 -3
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/dist/templates/CLAUDE.md
CHANGED
|
@@ -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/
|
|
22
|
-
├──
|
|
23
|
-
│ ├──
|
|
24
|
-
│ └──
|
|
25
|
-
├──
|
|
26
|
-
├──
|
|
27
|
-
├──
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
|
37
|
-
|
|
|
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:** `.
|
|
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 .
|
|
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., `
|
|
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:
|
|
123
|
-
| Workflows (Windsurf) | `
|
|
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/`)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
2
|
+
"""SessionStart hook for mode transitions and post-compaction 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
|
-
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
|
-
|
|
15
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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}")
|