aiwcli 0.9.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/README.md +1248 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +16 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +19 -0
- package/dist/commands/branch.d.ts +45 -0
- package/dist/commands/branch.js +488 -0
- package/dist/commands/clean.d.ts +34 -0
- package/dist/commands/clean.js +186 -0
- package/dist/commands/clear.d.ts +51 -0
- package/dist/commands/clear.js +835 -0
- package/dist/commands/init/index.d.ts +107 -0
- package/dist/commands/init/index.js +565 -0
- package/dist/commands/launch.d.ts +21 -0
- package/dist/commands/launch.js +108 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/base-command.d.ts +114 -0
- package/dist/lib/base-command.js +153 -0
- package/dist/lib/bmad-installer.d.ts +38 -0
- package/dist/lib/bmad-installer.js +145 -0
- package/dist/lib/claude-settings-types.d.ts +102 -0
- package/dist/lib/claude-settings-types.js +5 -0
- package/dist/lib/config.d.ts +25 -0
- package/dist/lib/config.js +46 -0
- package/dist/lib/debug.d.ts +39 -0
- package/dist/lib/debug.js +74 -0
- package/dist/lib/env-compat.d.ts +26 -0
- package/dist/lib/env-compat.js +35 -0
- package/dist/lib/errors.d.ts +126 -0
- package/dist/lib/errors.js +145 -0
- package/dist/lib/generic-merge.d.ts +74 -0
- package/dist/lib/generic-merge.js +105 -0
- package/dist/lib/git/branch.d.ts +67 -0
- package/dist/lib/git/branch.js +155 -0
- package/dist/lib/git/index.d.ts +11 -0
- package/dist/lib/git/index.js +13 -0
- package/dist/lib/git/safety-checks.d.ts +44 -0
- package/dist/lib/git/safety-checks.js +102 -0
- package/dist/lib/git/types.d.ts +31 -0
- package/dist/lib/git/types.js +6 -0
- package/dist/lib/git/worktree.d.ts +67 -0
- package/dist/lib/git/worktree.js +220 -0
- package/dist/lib/gitignore-manager.d.ts +10 -0
- package/dist/lib/gitignore-manager.js +60 -0
- package/dist/lib/hooks-merger.d.ts +28 -0
- package/dist/lib/hooks-merger.js +94 -0
- package/dist/lib/ide-path-resolver.d.ts +102 -0
- package/dist/lib/ide-path-resolver.js +129 -0
- package/dist/lib/index.d.ts +13 -0
- package/dist/lib/index.js +22 -0
- package/dist/lib/output.d.ts +51 -0
- package/dist/lib/output.js +76 -0
- package/dist/lib/paths.d.ts +66 -0
- package/dist/lib/paths.js +136 -0
- package/dist/lib/quiet.d.ts +12 -0
- package/dist/lib/quiet.js +17 -0
- package/dist/lib/settings-hierarchy.d.ts +42 -0
- package/dist/lib/settings-hierarchy.js +105 -0
- package/dist/lib/spawn.d.ts +105 -0
- package/dist/lib/spawn.js +157 -0
- package/dist/lib/spinner.d.ts +19 -0
- package/dist/lib/spinner.js +34 -0
- package/dist/lib/stdin.d.ts +48 -0
- package/dist/lib/stdin.js +60 -0
- package/dist/lib/template-installer.d.ts +92 -0
- package/dist/lib/template-installer.js +375 -0
- package/dist/lib/template-linter.d.ts +49 -0
- package/dist/lib/template-linter.js +173 -0
- package/dist/lib/template-merger.d.ts +47 -0
- package/dist/lib/template-merger.js +173 -0
- package/dist/lib/template-resolver.d.ts +20 -0
- package/dist/lib/template-resolver.js +60 -0
- package/dist/lib/terminal.d.ts +102 -0
- package/dist/lib/terminal.js +245 -0
- package/dist/lib/tty-detection.d.ts +62 -0
- package/dist/lib/tty-detection.js +83 -0
- package/dist/lib/user-utils.d.ts +5 -0
- package/dist/lib/user-utils.js +23 -0
- package/dist/lib/version.d.ts +99 -0
- package/dist/lib/version.js +144 -0
- package/dist/lib/watch-templates.d.ts +6 -0
- package/dist/lib/watch-templates.js +73 -0
- package/dist/lib/windsurf-hooks-hierarchy.d.ts +30 -0
- package/dist/lib/windsurf-hooks-hierarchy.js +66 -0
- package/dist/lib/windsurf-hooks-merger.d.ts +26 -0
- package/dist/lib/windsurf-hooks-merger.js +53 -0
- package/dist/lib/windsurf-hooks-types.d.ts +33 -0
- package/dist/lib/windsurf-hooks-types.js +5 -0
- package/dist/templates/CLAUDE.md +174 -0
- package/dist/templates/_shared/.claude/commands/handoff.md +14 -0
- package/dist/templates/_shared/.claude/settings.json +61 -0
- package/dist/templates/_shared/.codex/workflows/handoff.md +14 -0
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +14 -0
- package/dist/templates/_shared/hooks/__init__.py +16 -0
- package/dist/templates/_shared/hooks/archive_plan.py +270 -0
- package/dist/templates/_shared/hooks/context_enforcer.py +621 -0
- package/dist/templates/_shared/hooks/context_monitor.py +322 -0
- package/dist/templates/_shared/hooks/file-suggestion.py +188 -0
- package/dist/templates/_shared/hooks/task_create_capture.py +194 -0
- package/dist/templates/_shared/hooks/task_update_capture.py +254 -0
- package/dist/templates/_shared/hooks/user_prompt_submit.py +157 -0
- package/dist/templates/_shared/lib/__init__.py +1 -0
- package/dist/templates/_shared/lib/base/__init__.py +49 -0
- package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/atomic_write.py +180 -0
- package/dist/templates/_shared/lib/base/constants.py +299 -0
- package/dist/templates/_shared/lib/base/inference.py +189 -0
- package/dist/templates/_shared/lib/base/utils.py +216 -0
- package/dist/templates/_shared/lib/context/__init__.py +119 -0
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/cache.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__/event_log.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/cache.py +446 -0
- package/dist/templates/_shared/lib/context/context_manager.py +1171 -0
- package/dist/templates/_shared/lib/context/discovery.py +486 -0
- package/dist/templates/_shared/lib/context/event_log.py +308 -0
- package/dist/templates/_shared/lib/context/plan_archive.py +247 -0
- package/dist/templates/_shared/lib/context/task_sync.py +367 -0
- package/dist/templates/_shared/lib/handoff/__init__.py +22 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +307 -0
- package/dist/templates/_shared/lib/templates/README.md +215 -0
- package/dist/templates/_shared/lib/templates/__init__.py +40 -0
- package/dist/templates/_shared/lib/templates/formatters.py +147 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +119 -0
- package/dist/templates/_shared/scripts/save_handoff.py +99 -0
- package/dist/templates/_shared/workflows/handoff.md +212 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/ACCESSIBILITY-TESTER.md +80 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/ARCHITECT-REVIEWER.md +75 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/ASSUMPTION-CHAIN-TRACER.md +239 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/CLARITY-AUDITOR.md +109 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/CODE-REVIEWER.md +71 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/COMPLETENESS-CHECKER.md +104 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/CONTEXT-EXTRACTOR.md +93 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/DEVILS-ADVOCATE.md +223 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/DOCUMENTATION-REVIEWER.md +73 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/FEASIBILITY-ANALYST.md +93 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/FRESH-PERSPECTIVE.md +103 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/HANDOFF-READINESS.md +145 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/HIDDEN-COMPLEXITY-DETECTOR.md +248 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/INCENTIVE-MAPPER.md +235 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PENETRATION-TESTER.md +80 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PERFORMANCE-ENGINEER.md +76 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PLAN-ORCHESTRATOR.md +141 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PRECEDENT-FINDER.md +240 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/REVERSIBILITY-ANALYST.md +211 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/RISK-ASSESSOR.md +101 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/SECOND-ORDER-ANALYST.md +197 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/SIMPLICITY-GUARDIAN.md +97 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/SKEPTIC.md +349 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/STAKEHOLDER-ADVOCATE.md +106 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/TRADE-OFF-ILLUMINATOR.md +205 -0
- package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +8 -0
- package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -0
- package/dist/templates/cc-native/.claude/settings.json +119 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +8 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -0
- package/dist/templates/cc-native/CC-NATIVE-README.md +192 -0
- package/dist/templates/cc-native/MIGRATION.md +86 -0
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +331 -0
- package/dist/templates/cc-native/_cc-native/docs/PERMISSION_REQUEST_VERIFICATION.md +147 -0
- 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__/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__/cc-native-plan-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/hooks/add_plan_context.py +150 -0
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +746 -0
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +339 -0
- package/dist/templates/cc-native/_cc-native/lib/__init__.py +57 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.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/async_archive.py +68 -0
- package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +98 -0
- package/dist/templates/cc-native/_cc-native/lib/constants.py +45 -0
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +273 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +28 -0
- 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 +164 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +89 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +119 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +103 -0
- package/dist/templates/cc-native/_cc-native/lib/state.py +251 -0
- package/dist/templates/cc-native/_cc-native/lib/utils.py +830 -0
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +76 -0
- package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +151 -0
- package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +134 -0
- package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -0
- package/dist/types/exit-codes.d.ts +11 -0
- package/dist/types/exit-codes.js +10 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +7 -0
- package/oclif.manifest.json +405 -0
- package/package.json +109 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Context monitor hook for proactive handoff warnings.
|
|
3
|
+
|
|
4
|
+
This hook runs on PostToolUse for context-heavy tools and monitors
|
|
5
|
+
context window usage. When context drops below a threshold, it injects
|
|
6
|
+
a system reminder instructing Claude to wrap up and create a handoff document.
|
|
7
|
+
|
|
8
|
+
Unlike UserPromptSubmit hooks, this fires DURING Claude's work,
|
|
9
|
+
allowing proactive intervention without waiting for user input.
|
|
10
|
+
|
|
11
|
+
Monitored tools (configured via settings.json matcher):
|
|
12
|
+
- Task: Subagent responses can be huge
|
|
13
|
+
- Read: File content loads into context
|
|
14
|
+
- Bash: Command output can be large
|
|
15
|
+
- WebFetch: Web content loads into context
|
|
16
|
+
|
|
17
|
+
Hook input (from Claude Code):
|
|
18
|
+
{
|
|
19
|
+
"hook_event_name": "PostToolUse",
|
|
20
|
+
"tool_name": "Task",
|
|
21
|
+
"tool_input": {...},
|
|
22
|
+
"tool_result": {...},
|
|
23
|
+
"transcript_path": "/path/to/transcript.jsonl",
|
|
24
|
+
"session_id": "abc123",
|
|
25
|
+
"context_window": {
|
|
26
|
+
"current_usage": {
|
|
27
|
+
"cache_read_input_tokens": 0,
|
|
28
|
+
"input_tokens": 12345,
|
|
29
|
+
"cache_creation_input_tokens": 0,
|
|
30
|
+
"output_tokens": 6789
|
|
31
|
+
},
|
|
32
|
+
"context_window_size": 200000
|
|
33
|
+
},
|
|
34
|
+
...
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Hook output:
|
|
38
|
+
- Outputs JSON with additionalContext if context is low
|
|
39
|
+
- This injects a system reminder into Claude's context
|
|
40
|
+
- Plain stdout from PostToolUse only goes to verbose mode, not Claude
|
|
41
|
+
- Using additionalContext ensures Claude sees and responds to the warning
|
|
42
|
+
|
|
43
|
+
KNOWN LIMITATION: Context percentage won't match /context exactly.
|
|
44
|
+
Hook JSON excludes system prompt, tools, MCP tokens. We add a baseline
|
|
45
|
+
to compensate (~22.6k tokens typical). See:
|
|
46
|
+
https://github.com/anthropics/claude-code/issues/13783
|
|
47
|
+
"""
|
|
48
|
+
import json
|
|
49
|
+
import sys
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
from typing import Optional, Tuple
|
|
52
|
+
|
|
53
|
+
# Add parent directories to path for imports
|
|
54
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
55
|
+
SHARED_LIB = SCRIPT_DIR.parent / "lib"
|
|
56
|
+
sys.path.insert(0, str(SHARED_LIB.parent))
|
|
57
|
+
|
|
58
|
+
from lib.base.utils import eprint, project_dir
|
|
59
|
+
from lib.context.context_manager import (
|
|
60
|
+
get_all_contexts,
|
|
61
|
+
get_context_by_session_id,
|
|
62
|
+
update_plan_status,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Configuration
|
|
66
|
+
LOW_CONTEXT_THRESHOLD = 40 # Warn when below 40% remaining
|
|
67
|
+
CRITICAL_CONTEXT_THRESHOLD = 25 # Urgent warning below 25%
|
|
68
|
+
|
|
69
|
+
# Context baseline: preloaded tokens not visible to hooks (~22.6k typical)
|
|
70
|
+
# This includes system prompt, tools, MCP tokens that aren't in hook data
|
|
71
|
+
CONTEXT_BASELINE = 22_600
|
|
72
|
+
|
|
73
|
+
# Default context window size (used when not provided in hook input)
|
|
74
|
+
DEFAULT_CONTEXT_WINDOW = 200_000
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_context_tokens_from_hook(hook_input: dict) -> Tuple[Optional[int], Optional[int]]:
|
|
78
|
+
"""
|
|
79
|
+
Extract actual token counts from Claude Code hook input.
|
|
80
|
+
|
|
81
|
+
Claude Code provides context_window data with actual token counts:
|
|
82
|
+
- cache_read_input_tokens: Tokens read from cache
|
|
83
|
+
- input_tokens: New input tokens
|
|
84
|
+
- cache_creation_input_tokens: Tokens written to cache
|
|
85
|
+
- output_tokens: Model output tokens
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
hook_input: Hook input data from Claude Code
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Tuple of (tokens_used, max_tokens) or (None, None) if not available
|
|
92
|
+
"""
|
|
93
|
+
context_window = hook_input.get("context_window")
|
|
94
|
+
if not context_window:
|
|
95
|
+
return None, None
|
|
96
|
+
|
|
97
|
+
current_usage = context_window.get("current_usage")
|
|
98
|
+
if not current_usage:
|
|
99
|
+
return None, None
|
|
100
|
+
|
|
101
|
+
# Sum all token types
|
|
102
|
+
cache_read = current_usage.get("cache_read_input_tokens", 0) or 0
|
|
103
|
+
input_tokens = current_usage.get("input_tokens", 0) or 0
|
|
104
|
+
cache_creation = current_usage.get("cache_creation_input_tokens", 0) or 0
|
|
105
|
+
output_tokens = current_usage.get("output_tokens", 0) or 0
|
|
106
|
+
|
|
107
|
+
content_tokens = cache_read + input_tokens + cache_creation + output_tokens
|
|
108
|
+
|
|
109
|
+
# Add baseline for system prompt, tools, MCP tokens not in hook data
|
|
110
|
+
tokens_used = content_tokens + CONTEXT_BASELINE
|
|
111
|
+
|
|
112
|
+
# Get max context window from hook input
|
|
113
|
+
max_tokens = context_window.get("context_window_size") or DEFAULT_CONTEXT_WINDOW
|
|
114
|
+
|
|
115
|
+
return tokens_used, max_tokens
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_current_context_id(project_root: Path = None) -> Optional[str]:
|
|
119
|
+
"""
|
|
120
|
+
Determine the current active context.
|
|
121
|
+
|
|
122
|
+
Falls back to most recently active context.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Context ID or None if no active context
|
|
126
|
+
"""
|
|
127
|
+
contexts = get_all_contexts(status="active", project_root=project_root)
|
|
128
|
+
if contexts:
|
|
129
|
+
return contexts[0].id # Sorted by last_active desc
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_context_warning(
|
|
134
|
+
percent_remaining: int,
|
|
135
|
+
tokens_used: int,
|
|
136
|
+
max_tokens: int,
|
|
137
|
+
context_id: Optional[str],
|
|
138
|
+
tool_name: str
|
|
139
|
+
) -> str:
|
|
140
|
+
"""
|
|
141
|
+
Generate appropriate warning based on context level.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
percent_remaining: Percentage of context remaining
|
|
145
|
+
tokens_used: Estimated tokens used
|
|
146
|
+
max_tokens: Maximum context window
|
|
147
|
+
context_id: Current context ID (if any)
|
|
148
|
+
tool_name: Tool that triggered this check
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
System reminder markdown
|
|
152
|
+
"""
|
|
153
|
+
# Format token counts
|
|
154
|
+
tokens_used_k = tokens_used // 1000
|
|
155
|
+
max_tokens_k = max_tokens // 1000
|
|
156
|
+
|
|
157
|
+
if percent_remaining <= CRITICAL_CONTEXT_THRESHOLD:
|
|
158
|
+
urgency = "CRITICAL"
|
|
159
|
+
instruction = "You MUST wrap up immediately and create a handoff document."
|
|
160
|
+
else:
|
|
161
|
+
urgency = "LOW"
|
|
162
|
+
instruction = "Please wrap up your current task and prepare for handoff."
|
|
163
|
+
|
|
164
|
+
context_info = ""
|
|
165
|
+
if context_id:
|
|
166
|
+
context_info = f"""
|
|
167
|
+
To create a handoff document, use the /handoff command or describe:
|
|
168
|
+
- What you were working on
|
|
169
|
+
- What's completed
|
|
170
|
+
- What still needs to be done
|
|
171
|
+
- Any important decisions or context
|
|
172
|
+
|
|
173
|
+
Context ID: `{context_id}`"""
|
|
174
|
+
|
|
175
|
+
return f"""<system-reminder>
|
|
176
|
+
## {urgency} CONTEXT WARNING ({percent_remaining}% remaining)
|
|
177
|
+
|
|
178
|
+
**Estimated usage**: ~{tokens_used_k}k / {max_tokens_k}k tokens
|
|
179
|
+
**Triggered by**: {tool_name} tool completion
|
|
180
|
+
|
|
181
|
+
{instruction}
|
|
182
|
+
{context_info}
|
|
183
|
+
|
|
184
|
+
**Actions:**
|
|
185
|
+
1. Complete your current atomic task (if 1-2 steps away)
|
|
186
|
+
2. Do NOT start new multi-step work
|
|
187
|
+
3. Create a handoff document summarizing progress
|
|
188
|
+
4. Ask user: "Context is getting low. I've summarized my progress. Should we continue in a new session?"
|
|
189
|
+
</system-reminder>"""
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def check_and_transition_mode(hook_input: dict) -> None:
|
|
193
|
+
"""
|
|
194
|
+
Check if context needs to transition from pending_implementation to implementing.
|
|
195
|
+
|
|
196
|
+
This handles the case where a plan was approved and implementation has started,
|
|
197
|
+
but the context mode wasn't updated. If we're seeing tool usage (Edit, Write, Bash)
|
|
198
|
+
and the context is in "pending_implementation", we transition to "implementing".
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
hook_input: Hook input data from Claude Code
|
|
202
|
+
"""
|
|
203
|
+
# Only transition on tools that indicate implementation work
|
|
204
|
+
implementation_tools = {"Edit", "Write", "Bash", "NotebookEdit"}
|
|
205
|
+
tool_name = hook_input.get("tool_name", "")
|
|
206
|
+
|
|
207
|
+
if tool_name not in implementation_tools:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
project_root = project_dir(hook_input)
|
|
211
|
+
session_id = hook_input.get("session_id")
|
|
212
|
+
|
|
213
|
+
if not session_id:
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
# Get context for this session
|
|
217
|
+
context = get_context_by_session_id(session_id, project_root)
|
|
218
|
+
if not context:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
# Check if we need to transition
|
|
222
|
+
if context.in_flight and context.in_flight.mode == "pending_implementation":
|
|
223
|
+
eprint(f"[context_monitor] Transitioning {context.id} from pending_implementation to implementing")
|
|
224
|
+
update_plan_status(context.id, "implementing", project_root=project_root)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def check_context_level(hook_input: dict) -> Optional[str]:
|
|
228
|
+
"""
|
|
229
|
+
Check context level and return warning if low.
|
|
230
|
+
|
|
231
|
+
Optimized for fail-fast: checks cheap conditions first before any file I/O.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
hook_input: Hook input data from Claude Code
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
System reminder string if context is low, None otherwise
|
|
238
|
+
"""
|
|
239
|
+
# === FAST PATH: No I/O, just dict lookups and math ===
|
|
240
|
+
|
|
241
|
+
# 1. Try to get context_window data (fast dict access)
|
|
242
|
+
tokens_used, max_tokens = get_context_tokens_from_hook(hook_input)
|
|
243
|
+
|
|
244
|
+
# 2. If no context_window data, exit immediately - can't monitor accurately
|
|
245
|
+
if tokens_used is None or max_tokens is None:
|
|
246
|
+
# Only log if we want to debug
|
|
247
|
+
# eprint("[context_monitor] context_window data unavailable")
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
# 3. Calculate percentage (fast math)
|
|
251
|
+
remaining = max_tokens - tokens_used
|
|
252
|
+
percent_remaining = max(0, min(100, int((remaining / max_tokens) * 100)))
|
|
253
|
+
|
|
254
|
+
# 4. Most common case: context is fine, exit early
|
|
255
|
+
if percent_remaining > LOW_CONTEXT_THRESHOLD:
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
# === SLOW PATH: Only reached when context is low (rare) ===
|
|
259
|
+
|
|
260
|
+
# Log since we're in warning territory
|
|
261
|
+
eprint(f"[context_monitor] Context: {percent_remaining}% remaining "
|
|
262
|
+
f"(~{tokens_used//1000}k/{max_tokens//1000}k tokens)")
|
|
263
|
+
|
|
264
|
+
# Check mode transition (file I/O)
|
|
265
|
+
check_and_transition_mode(hook_input)
|
|
266
|
+
|
|
267
|
+
# Get current context for handoff info (file I/O)
|
|
268
|
+
project_root = project_dir(hook_input)
|
|
269
|
+
context_id = get_current_context_id(project_root)
|
|
270
|
+
|
|
271
|
+
tool_name = hook_input.get("tool_name", "Unknown")
|
|
272
|
+
|
|
273
|
+
return get_context_warning(
|
|
274
|
+
percent_remaining,
|
|
275
|
+
tokens_used,
|
|
276
|
+
max_tokens,
|
|
277
|
+
context_id,
|
|
278
|
+
tool_name
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def main():
|
|
283
|
+
"""
|
|
284
|
+
Main entry point for PostToolUse hook.
|
|
285
|
+
|
|
286
|
+
Reads hook input from stdin, estimates context usage,
|
|
287
|
+
and prints system reminder if context is low.
|
|
288
|
+
"""
|
|
289
|
+
try:
|
|
290
|
+
# Read hook input from stdin
|
|
291
|
+
input_data = sys.stdin.read().strip()
|
|
292
|
+
|
|
293
|
+
if not input_data:
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
hook_input = json.loads(input_data)
|
|
298
|
+
except json.JSONDecodeError:
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
# Check context level
|
|
302
|
+
warning = check_context_level(hook_input)
|
|
303
|
+
|
|
304
|
+
if warning:
|
|
305
|
+
# Output JSON with additionalContext so Claude sees the warning
|
|
306
|
+
# Plain stdout from PostToolUse only goes to verbose mode, not Claude's context
|
|
307
|
+
output = {
|
|
308
|
+
"hookSpecificOutput": {
|
|
309
|
+
"hookEventName": "PostToolUse",
|
|
310
|
+
"additionalContext": warning
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
print(json.dumps(output))
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
eprint(f"[context_monitor] ERROR: {e}")
|
|
317
|
+
import traceback
|
|
318
|
+
eprint(traceback.format_exc())
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
if __name__ == "__main__":
|
|
322
|
+
main()
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""File suggestion hook for Claude Code.
|
|
3
|
+
|
|
4
|
+
Suggests relevant files to include in context based on the current session:
|
|
5
|
+
- Context file (context.json) for the active context
|
|
6
|
+
- Plans from the active context's plans/ directory
|
|
7
|
+
- Handoffs from the active context's handoffs/ directory
|
|
8
|
+
- Reviews from the active context's reviews/ directory (including cc-native subdirectory)
|
|
9
|
+
|
|
10
|
+
Hook input (from Claude Code):
|
|
11
|
+
{
|
|
12
|
+
"session_id": "abc123",
|
|
13
|
+
"cwd": "/path/to/project",
|
|
14
|
+
...
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Hook output:
|
|
18
|
+
JSON array of file paths to suggest, or empty array if no suggestions.
|
|
19
|
+
["/path/to/file1.md", "/path/to/file2.md"]
|
|
20
|
+
"""
|
|
21
|
+
import json
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import List, Optional
|
|
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.utils import eprint, project_dir
|
|
32
|
+
from lib.base.constants import (
|
|
33
|
+
get_context_plans_dir,
|
|
34
|
+
get_context_handoffs_dir,
|
|
35
|
+
get_context_reviews_dir,
|
|
36
|
+
get_context_file_path,
|
|
37
|
+
)
|
|
38
|
+
from lib.context.context_manager import (
|
|
39
|
+
get_context_by_session_id,
|
|
40
|
+
get_all_in_flight_contexts,
|
|
41
|
+
get_context,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_context_files(context_id: str, project_root: Path) -> List[str]:
|
|
46
|
+
"""
|
|
47
|
+
Get all relevant files for a context.
|
|
48
|
+
|
|
49
|
+
Collects:
|
|
50
|
+
- Context file (context.json)
|
|
51
|
+
- Plans (most recent first)
|
|
52
|
+
- Handoffs (most recent first)
|
|
53
|
+
- Reviews (most recent first)
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
context_id: Context identifier
|
|
57
|
+
project_root: Project root path
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
List of absolute file paths, sorted by modification time (most recent first)
|
|
61
|
+
"""
|
|
62
|
+
files = []
|
|
63
|
+
|
|
64
|
+
# Get context.json file first
|
|
65
|
+
context_file = get_context_file_path(context_id, project_root)
|
|
66
|
+
if context_file.exists():
|
|
67
|
+
files.append(str(context_file))
|
|
68
|
+
eprint(f"[file-suggestion] Found context file for {context_id}")
|
|
69
|
+
|
|
70
|
+
# Get plans directory
|
|
71
|
+
plans_dir = get_context_plans_dir(context_id, project_root)
|
|
72
|
+
if plans_dir.exists():
|
|
73
|
+
plan_files = list(plans_dir.glob("*.md"))
|
|
74
|
+
# Sort by modification time, most recent first
|
|
75
|
+
plan_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
|
76
|
+
files.extend([str(p) for p in plan_files])
|
|
77
|
+
eprint(f"[file-suggestion] Found {len(plan_files)} plans in {context_id}")
|
|
78
|
+
|
|
79
|
+
# Get handoffs directory
|
|
80
|
+
handoffs_dir = get_context_handoffs_dir(context_id, project_root)
|
|
81
|
+
if handoffs_dir.exists():
|
|
82
|
+
handoff_files = list(handoffs_dir.glob("*.md"))
|
|
83
|
+
handoff_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
|
84
|
+
files.extend([str(p) for p in handoff_files])
|
|
85
|
+
eprint(f"[file-suggestion] Found {len(handoff_files)} handoffs in {context_id}")
|
|
86
|
+
|
|
87
|
+
# Get reviews directory (includes cc-native subdirectory)
|
|
88
|
+
reviews_dir = get_context_reviews_dir(context_id, project_root)
|
|
89
|
+
if reviews_dir.exists():
|
|
90
|
+
# Find review.md files in reviews/ and subdirectories (e.g., reviews/cc-native/review.md)
|
|
91
|
+
review_files = list(reviews_dir.glob("**/review.md"))
|
|
92
|
+
review_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
|
93
|
+
files.extend([str(p) for p in review_files])
|
|
94
|
+
eprint(f"[file-suggestion] Found {len(review_files)} reviews in {context_id}")
|
|
95
|
+
|
|
96
|
+
return files
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_active_context_id(session_id: str, project_root: Path) -> Optional[str]:
|
|
100
|
+
"""
|
|
101
|
+
Determine the active context for suggestions.
|
|
102
|
+
|
|
103
|
+
Priority:
|
|
104
|
+
1. Context bound to current session_id
|
|
105
|
+
2. Single in-flight context (if only one exists)
|
|
106
|
+
3. None (no suggestions if ambiguous)
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
session_id: Current session identifier
|
|
110
|
+
project_root: Project root path
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Context ID or None
|
|
114
|
+
"""
|
|
115
|
+
# Try session_id lookup first
|
|
116
|
+
if session_id and session_id != "unknown":
|
|
117
|
+
context = get_context_by_session_id(session_id, project_root)
|
|
118
|
+
if context:
|
|
119
|
+
eprint(f"[file-suggestion] Found context by session: {context.id}")
|
|
120
|
+
return context.id
|
|
121
|
+
|
|
122
|
+
# Fall back to single in-flight context
|
|
123
|
+
in_flight = get_all_in_flight_contexts(project_root)
|
|
124
|
+
if len(in_flight) == 1:
|
|
125
|
+
eprint(f"[file-suggestion] Using single in-flight context: {in_flight[0].id}")
|
|
126
|
+
return in_flight[0].id
|
|
127
|
+
|
|
128
|
+
eprint(f"[file-suggestion] No unique context found (in-flight: {len(in_flight)})")
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main():
|
|
133
|
+
"""
|
|
134
|
+
Main entry point for file suggestion hook.
|
|
135
|
+
|
|
136
|
+
Reads hook input from stdin, determines active context,
|
|
137
|
+
and outputs file suggestions as JSON array.
|
|
138
|
+
"""
|
|
139
|
+
try:
|
|
140
|
+
# Read hook input from stdin
|
|
141
|
+
input_data = sys.stdin.read().strip()
|
|
142
|
+
|
|
143
|
+
if not input_data:
|
|
144
|
+
print("[]")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
hook_input = json.loads(input_data)
|
|
149
|
+
except json.JSONDecodeError:
|
|
150
|
+
eprint("[file-suggestion] Failed to parse input JSON")
|
|
151
|
+
print("[]")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
# Get project root and session ID
|
|
155
|
+
project_root = project_dir(hook_input)
|
|
156
|
+
session_id = hook_input.get("session_id", "unknown")
|
|
157
|
+
|
|
158
|
+
eprint(f"[file-suggestion] Session: {session_id[:8]}..., Project: {project_root}")
|
|
159
|
+
|
|
160
|
+
# Determine active context
|
|
161
|
+
context_id = get_active_context_id(session_id, project_root)
|
|
162
|
+
|
|
163
|
+
if not context_id:
|
|
164
|
+
print("[]")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# Collect file suggestions
|
|
168
|
+
suggestions = get_context_files(context_id, project_root)
|
|
169
|
+
|
|
170
|
+
# Limit suggestions to prevent overwhelming the context
|
|
171
|
+
MAX_SUGGESTIONS = 10
|
|
172
|
+
if len(suggestions) > MAX_SUGGESTIONS:
|
|
173
|
+
eprint(f"[file-suggestion] Limiting suggestions to {MAX_SUGGESTIONS} (was {len(suggestions)})")
|
|
174
|
+
suggestions = suggestions[:MAX_SUGGESTIONS]
|
|
175
|
+
|
|
176
|
+
# Output suggestions as JSON array
|
|
177
|
+
eprint(f"[file-suggestion] Suggesting {len(suggestions)} files")
|
|
178
|
+
print(json.dumps(suggestions))
|
|
179
|
+
|
|
180
|
+
except Exception as e:
|
|
181
|
+
eprint(f"[file-suggestion] ERROR: {e}")
|
|
182
|
+
import traceback
|
|
183
|
+
eprint(traceback.format_exc())
|
|
184
|
+
print("[]")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
main()
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PostToolUse hook - captures TaskCreate operations for persistence.
|
|
3
|
+
|
|
4
|
+
This hook runs after Claude uses the TaskCreate tool and automatically
|
|
5
|
+
records the task creation event in the context's events.jsonl.
|
|
6
|
+
|
|
7
|
+
Hook input (from Claude Code):
|
|
8
|
+
{
|
|
9
|
+
"hook_event_name": "PostToolUse",
|
|
10
|
+
"tool_name": "TaskCreate",
|
|
11
|
+
"tool_input": {
|
|
12
|
+
"subject": "Task subject",
|
|
13
|
+
"description": "Task description",
|
|
14
|
+
"activeForm": "Present continuous form",
|
|
15
|
+
"metadata": {"context": "context-id", ...}
|
|
16
|
+
},
|
|
17
|
+
"tool_response": {"task": {"id": "1", "subject": "..."}},
|
|
18
|
+
"session_id": "abc123",
|
|
19
|
+
"cwd": "/path/to/project"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Hook output:
|
|
23
|
+
- Silent on success (no stdout output)
|
|
24
|
+
- Logs to stderr for debugging
|
|
25
|
+
"""
|
|
26
|
+
import json
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Optional, Dict, Any
|
|
30
|
+
|
|
31
|
+
# Add parent directories to path for imports
|
|
32
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
33
|
+
SHARED_LIB = SCRIPT_DIR.parent / "lib"
|
|
34
|
+
sys.path.insert(0, str(SHARED_LIB.parent))
|
|
35
|
+
|
|
36
|
+
from lib.context.task_sync import record_task_created, generate_next_task_id
|
|
37
|
+
from lib.context.context_manager import get_all_contexts, get_context_by_session_id
|
|
38
|
+
from lib.base.utils import eprint, project_dir
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def extract_context_id(
|
|
42
|
+
tool_input: Dict[str, Any],
|
|
43
|
+
project_root: Path,
|
|
44
|
+
session_id: Optional[str] = None
|
|
45
|
+
) -> Optional[str]:
|
|
46
|
+
"""
|
|
47
|
+
Extract context ID from tool input metadata, session, or active contexts.
|
|
48
|
+
|
|
49
|
+
Priority:
|
|
50
|
+
1. metadata.context field
|
|
51
|
+
2. Session ID lookup (session bound to context)
|
|
52
|
+
3. metadata.persistent_id prefix (e.g., "ctx-123-task-1" -> "ctx-123")
|
|
53
|
+
4. Single active context
|
|
54
|
+
5. None (will trigger auto-creation)
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
tool_input: Tool input from TaskCreate
|
|
58
|
+
project_root: Project root directory
|
|
59
|
+
session_id: Session ID from hook payload
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Context ID or None if cannot determine
|
|
63
|
+
"""
|
|
64
|
+
# Check metadata.context field
|
|
65
|
+
metadata = tool_input.get("metadata", {})
|
|
66
|
+
if isinstance(metadata, dict):
|
|
67
|
+
context = metadata.get("context")
|
|
68
|
+
if context:
|
|
69
|
+
return context
|
|
70
|
+
|
|
71
|
+
# Check session ID - session may be bound to a context
|
|
72
|
+
if session_id:
|
|
73
|
+
try:
|
|
74
|
+
session_context = get_context_by_session_id(session_id, project_root)
|
|
75
|
+
if session_context:
|
|
76
|
+
eprint(f"[task_create_capture] Found context via session_id: {session_context.id}")
|
|
77
|
+
return session_context.id
|
|
78
|
+
except Exception as e:
|
|
79
|
+
eprint(f"[task_create_capture] Failed to lookup context by session: {e}")
|
|
80
|
+
|
|
81
|
+
# Check persistent_id for context hint
|
|
82
|
+
if isinstance(metadata, dict):
|
|
83
|
+
persistent_id = metadata.get("persistent_id", "")
|
|
84
|
+
if persistent_id and "-" in persistent_id:
|
|
85
|
+
# Format: "context-id-task-1" or similar
|
|
86
|
+
parts = persistent_id.split("-")
|
|
87
|
+
if len(parts) >= 2:
|
|
88
|
+
# Reconstruct context ID (everything before last two parts)
|
|
89
|
+
context_parts = parts[:-2] if len(parts) > 2 else parts[:1]
|
|
90
|
+
return "-".join(context_parts)
|
|
91
|
+
|
|
92
|
+
# Check for single active context
|
|
93
|
+
try:
|
|
94
|
+
contexts = get_all_contexts(status="active", project_root=project_root)
|
|
95
|
+
if len(contexts) == 1:
|
|
96
|
+
return contexts[0].id
|
|
97
|
+
except Exception as e:
|
|
98
|
+
eprint(f"[task_create_capture] Failed to get active contexts: {e}")
|
|
99
|
+
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def main() -> int:
|
|
104
|
+
"""
|
|
105
|
+
Main hook entry point.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
0 on success, non-zero on failure (but hook is non-blocking)
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
# Parse hook input
|
|
112
|
+
payload = json.load(sys.stdin)
|
|
113
|
+
|
|
114
|
+
# Validate hook type
|
|
115
|
+
if payload.get("hook_event_name") != "PostToolUse":
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
# Validate tool name
|
|
119
|
+
if payload.get("tool_name") != "TaskCreate":
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
# Extract tool input
|
|
123
|
+
tool_input = payload.get("tool_input", {})
|
|
124
|
+
if not isinstance(tool_input, dict):
|
|
125
|
+
eprint("[task_create_capture] Invalid tool_input: not a dict")
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
# Check for skip_persistence flag (used during hydration to avoid duplicates)
|
|
129
|
+
metadata = tool_input.get("metadata", {})
|
|
130
|
+
if isinstance(metadata, dict) and metadata.get("skip_persistence"):
|
|
131
|
+
eprint("[task_create_capture] Skipping persistence (hydration mode)")
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
# Extract tool response (contains task ID assigned by Claude)
|
|
135
|
+
tool_response = payload.get("tool_response", {})
|
|
136
|
+
if not isinstance(tool_response, dict):
|
|
137
|
+
eprint("[task_create_capture] Invalid tool_response: not a dict")
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
# Get project root and session ID
|
|
141
|
+
project_root = project_dir(payload)
|
|
142
|
+
session_id = payload.get("session_id")
|
|
143
|
+
|
|
144
|
+
# Extract context ID
|
|
145
|
+
context_id = extract_context_id(tool_input, project_root, session_id)
|
|
146
|
+
if not context_id:
|
|
147
|
+
eprint("[task_create_capture] No context available - skipping persistence")
|
|
148
|
+
eprint("[task_create_capture] Task will be ephemeral until context is created")
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
# Extract task data
|
|
152
|
+
subject = tool_input.get("subject", "")
|
|
153
|
+
if not subject:
|
|
154
|
+
eprint("[task_create_capture] Missing required field: subject")
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
description = tool_input.get("description", "")
|
|
158
|
+
active_form = tool_input.get("activeForm", "")
|
|
159
|
+
|
|
160
|
+
# Generate persistent task ID
|
|
161
|
+
# Claude's native ID is ephemeral (1, 2, 3...)
|
|
162
|
+
# We need a persistent ID that survives sessions
|
|
163
|
+
persistent_task_id = generate_next_task_id(context_id, project_root)
|
|
164
|
+
|
|
165
|
+
# Record the task creation event
|
|
166
|
+
success = record_task_created(
|
|
167
|
+
context_id=context_id,
|
|
168
|
+
task_id=persistent_task_id,
|
|
169
|
+
subject=subject,
|
|
170
|
+
description=description,
|
|
171
|
+
active_form=active_form,
|
|
172
|
+
project_root=project_root
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if success:
|
|
176
|
+
eprint(f"[task_create_capture] Recorded task_added: {persistent_task_id} in {context_id}")
|
|
177
|
+
else:
|
|
178
|
+
eprint(f"[task_create_capture] Failed to record task_added: {persistent_task_id}")
|
|
179
|
+
|
|
180
|
+
# Silent success (no stdout output)
|
|
181
|
+
return 0
|
|
182
|
+
|
|
183
|
+
except json.JSONDecodeError as e:
|
|
184
|
+
eprint(f"[task_create_capture] JSON decode error: {e}")
|
|
185
|
+
return 0 # Non-blocking
|
|
186
|
+
except Exception as e:
|
|
187
|
+
eprint(f"[task_create_capture] Unexpected error: {e}")
|
|
188
|
+
import traceback
|
|
189
|
+
eprint(traceback.format_exc())
|
|
190
|
+
return 0 # Non-blocking
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
raise SystemExit(main())
|