aiwcli 0.9.8 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/run.js +5 -2
- package/dist/lib/claude-settings-types.d.ts +2 -0
- package/dist/templates/CLAUDE.md +3 -3
- package/dist/templates/_shared/.claude/settings.json +4 -0
- package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/archive_plan.py +87 -178
- package/dist/templates/_shared/hooks/context_monitor.py +104 -247
- package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
- package/dist/templates/_shared/hooks/pre_compact.py +47 -32
- package/dist/templates/_shared/hooks/session_end.py +103 -60
- package/dist/templates/_shared/hooks/session_start.py +110 -81
- package/dist/templates/_shared/hooks/task_create_capture.py +26 -50
- package/dist/templates/_shared/hooks/task_update_capture.py +42 -115
- package/dist/templates/_shared/hooks/user_prompt_submit.py +61 -61
- package/dist/templates/_shared/lib/base/__init__.py +16 -0
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
- package/dist/templates/_shared/lib/base/inference.py +121 -0
- package/dist/templates/_shared/lib/base/logger.py +291 -0
- package/dist/templates/_shared/lib/base/utils.py +42 -9
- package/dist/templates/_shared/lib/context/__init__.py +72 -80
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
- package/dist/templates/_shared/lib/context/context_selector.py +491 -0
- package/dist/templates/_shared/lib/context/context_store.py +636 -0
- package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
- package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
- package/dist/templates/_shared/lib/templates/README.md +5 -13
- package/dist/templates/_shared/lib/templates/__init__.py +2 -6
- package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +1 -38
- package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.py +39 -19
- package/dist/templates/_shared/scripts/status_line.py +701 -0
- package/dist/templates/_shared/workflows/handoff.md +9 -3
- package/dist/templates/cc-native/.claude/settings.json +41 -8
- package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
- package/dist/templates/cc-native/MIGRATION.md +1 -1
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +49 -21
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -55
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +163 -131
- package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +6 -4
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +34 -29
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
- package/dist/templates/cc-native/_cc-native/lib/utils.py +207 -40
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
- package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -177
- package/dist/templates/_shared/lib/context/auto_state.py +0 -167
- package/dist/templates/_shared/lib/context/cache.py +0 -444
- package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
- package/dist/templates/_shared/lib/context/context_manager.py +0 -1057
- package/dist/templates/_shared/lib/context/discovery.py +0 -554
- package/dist/templates/_shared/lib/context/event_log.py +0 -316
- package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
- package/dist/templates/_shared/lib/context/task_sync.py +0 -407
- package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
- package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +0 -98
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PostToolUse hook for ExitPlanMode — assigns plan fields to state.json.
|
|
3
|
+
|
|
4
|
+
This hook fires when the user ACCEPTS the plan (ExitPlanMode succeeds).
|
|
5
|
+
PostToolUse fires ONLY on success, so this reliably indicates acceptance.
|
|
6
|
+
|
|
7
|
+
The plan was already archived by archive_plan.py (PermissionRequest).
|
|
8
|
+
This hook finds the archived plan, computes hash + signature, and assigns
|
|
9
|
+
plan_hash, plan_signature, plan_path to state.json — without changing mode.
|
|
10
|
+
|
|
11
|
+
Separation of concerns:
|
|
12
|
+
- archive_plan.py (PermissionRequest) -> archives file only, no state.json changes
|
|
13
|
+
- plan_accepted.py (PostToolUse) -> assigns plan fields (hash/signature/path)
|
|
14
|
+
- session_end.py (SessionEnd) -> transitions active -> has_plan when plan is assigned
|
|
15
|
+
- context_selector.py -> matches plan content, transitions has_plan -> active
|
|
16
|
+
|
|
17
|
+
Usage in .claude/settings.json:
|
|
18
|
+
{
|
|
19
|
+
"hooks": {
|
|
20
|
+
"PostToolUse": [{
|
|
21
|
+
"matcher": "ExitPlanMode",
|
|
22
|
+
"hooks": [{
|
|
23
|
+
"type": "command",
|
|
24
|
+
"command": "python .aiwcli/_cc-native/hooks/plan_accepted.py",
|
|
25
|
+
"timeout": 5000
|
|
26
|
+
}]
|
|
27
|
+
}]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
"""
|
|
31
|
+
import hashlib
|
|
32
|
+
import sys
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
# Add parent directories to path for imports
|
|
36
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
37
|
+
SHARED_LIB = SCRIPT_DIR.parent.parent / "_shared" / "lib"
|
|
38
|
+
sys.path.insert(0, str(SHARED_LIB.parent))
|
|
39
|
+
|
|
40
|
+
from lib.base.hook_utils import load_hook_input, log_hook_error
|
|
41
|
+
from lib.base.logger import log_info, log_debug, log_warn, log_error
|
|
42
|
+
from lib.base.utils import project_dir
|
|
43
|
+
from lib.context.context_store import get_context_by_session_id, update_mode
|
|
44
|
+
from lib.context.plan_manager import (
|
|
45
|
+
find_latest_plan,
|
|
46
|
+
extract_plan_path_from_result,
|
|
47
|
+
generate_plan_id,
|
|
48
|
+
normalize_plan_content,
|
|
49
|
+
extract_plan_anchors,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main():
|
|
54
|
+
"""Assign plan fields (hash/signature/path) to state.json on plan acceptance."""
|
|
55
|
+
hook_input = load_hook_input()
|
|
56
|
+
if not hook_input:
|
|
57
|
+
log_warn("plan_accepted", "EXIT: no hook_input (stdin empty or invalid JSON)")
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
hook_event = hook_input.get("hook_event_name", "")
|
|
61
|
+
tool_name = hook_input.get("tool_name", "")
|
|
62
|
+
session_id = hook_input.get("session_id", "MISSING")
|
|
63
|
+
|
|
64
|
+
if not (hook_event == "PostToolUse" and tool_name == "ExitPlanMode"):
|
|
65
|
+
log_debug("plan_accepted", f"Skipping: {hook_event}/{tool_name}")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
project_root = project_dir(hook_input)
|
|
69
|
+
state = get_context_by_session_id(session_id, project_root)
|
|
70
|
+
if not state:
|
|
71
|
+
log_warn("plan_accepted", f"No context for session {session_id}")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
log_debug("plan_accepted", f"Found context: {state.id}, mode: {state.mode}")
|
|
75
|
+
|
|
76
|
+
# Find the latest archived plan
|
|
77
|
+
plan_path = find_latest_plan(state.id, project_root)
|
|
78
|
+
if not plan_path:
|
|
79
|
+
log_warn("plan_accepted", f"No archived plan found for {state.id}")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
# Find the original plan file (in ~/.claude/plans/) to inject plan-id there too
|
|
83
|
+
original_plan_path = extract_plan_path_from_result(hook_input.get("tool_result", ""))
|
|
84
|
+
if not original_plan_path:
|
|
85
|
+
claude_plans_dir = Path.home() / ".claude" / "plans"
|
|
86
|
+
if claude_plans_dir.exists():
|
|
87
|
+
plans = sorted(claude_plans_dir.glob("*.md"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
88
|
+
if plans:
|
|
89
|
+
original_plan_path = str(plans[0])
|
|
90
|
+
|
|
91
|
+
# Generate plan ID and inject into plan files
|
|
92
|
+
plan_id = generate_plan_id()
|
|
93
|
+
id_marker = f"<!-- plan-id: {plan_id} -->\n"
|
|
94
|
+
|
|
95
|
+
for file_path in [plan_path, original_plan_path]:
|
|
96
|
+
if file_path and Path(file_path).exists():
|
|
97
|
+
file_content = Path(file_path).read_text(encoding="utf-8")
|
|
98
|
+
if "<!-- plan-id:" not in file_content:
|
|
99
|
+
Path(file_path).write_text(id_marker + file_content, encoding="utf-8")
|
|
100
|
+
|
|
101
|
+
# Read the modified content (with plan ID) for hashing
|
|
102
|
+
content = Path(plan_path).read_text(encoding="utf-8")
|
|
103
|
+
|
|
104
|
+
# Compute normalized hash (Tier 2)
|
|
105
|
+
normalized = normalize_plan_content(content)
|
|
106
|
+
plan_hash = hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:12]
|
|
107
|
+
plan_signature = content[:200] # Keep for backward compat
|
|
108
|
+
|
|
109
|
+
# Extract structural anchors (Tier 3)
|
|
110
|
+
plan_anchors = extract_plan_anchors(content)
|
|
111
|
+
|
|
112
|
+
# Assign plan to context (no mode change — keep current mode)
|
|
113
|
+
update_mode(
|
|
114
|
+
state.id, state.mode,
|
|
115
|
+
project_root=project_root,
|
|
116
|
+
plan_path=plan_path,
|
|
117
|
+
plan_hash=plan_hash,
|
|
118
|
+
plan_signature=plan_signature,
|
|
119
|
+
plan_id=plan_id,
|
|
120
|
+
plan_anchors=plan_anchors,
|
|
121
|
+
)
|
|
122
|
+
log_info("plan_accepted", f"Assigned plan to {state.id} (id: {plan_id}, hash: {plan_hash}, anchors: {len(plan_anchors)})")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
from lib.base.hook_utils import run_hook
|
|
127
|
+
run_hook(main, "plan_accepted")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""UserPromptSubmit hook - injects Phase A clarification prompt in plan mode.
|
|
3
|
+
|
|
4
|
+
On the first prompt in plan mode (before any code exploration), injects
|
|
5
|
+
a system-reminder telling Claude to ask clarification questions via
|
|
6
|
+
AskUserQuestion before exploring the codebase.
|
|
7
|
+
|
|
8
|
+
Skips if questions were already asked this session.
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
_hook_dir = Path(__file__).resolve().parent
|
|
15
|
+
_cc_native_lib_dir = _hook_dir.parent / "lib"
|
|
16
|
+
_shared_lib_dir = _hook_dir.parent.parent / "_shared" / "lib"
|
|
17
|
+
sys.path.insert(0, str(_cc_native_lib_dir))
|
|
18
|
+
sys.path.insert(0, str(_shared_lib_dir))
|
|
19
|
+
|
|
20
|
+
from utils import was_questions_asked
|
|
21
|
+
from base.hook_utils import load_hook_input
|
|
22
|
+
from base.logger import log_debug, log_info, log_warn, log_error
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
PHASE_A_PROMPT = """
|
|
26
|
+
## Plan Mode: Clarify Before Exploring
|
|
27
|
+
|
|
28
|
+
Use AskUserQuestion now — one call, 3-4 questions — before reading any code.
|
|
29
|
+
|
|
30
|
+
### Why This Matters
|
|
31
|
+
Once you explore the codebase, you anchor on what you find. Questions asked after exploration confirm your assumptions instead of challenging them. Ask now, while your interpretation is still flexible.
|
|
32
|
+
|
|
33
|
+
### What to Ask About
|
|
34
|
+
Only ask about things you cannot discover from code — the user's intent, constraints, history, and priorities:
|
|
35
|
+
|
|
36
|
+
- **Ambiguity:** If you can read this request two different ways, ask which interpretation is correct. Provide your top 2-3 readings as options.
|
|
37
|
+
- **Invisible context:** What does the user assume "everyone knows" about this system that isn't documented? What's obvious to them but hidden to you?
|
|
38
|
+
- **Success criteria:** What does "done well" look like beyond the literal request? What would make them rate this a 10?
|
|
39
|
+
- **Constraints and history:** Has this been attempted before? Are there parts of the system that are off-limits or sensitive?
|
|
40
|
+
|
|
41
|
+
### How to Select Questions
|
|
42
|
+
1. Generate 5+ candidate questions across the lenses above
|
|
43
|
+
2. For each, evaluate: "If they answered A vs B, would I explore different files or take a different approach?" If no — discard it.
|
|
44
|
+
3. Keep the 3-4 where different answers lead to meaningfully different exploration strategies
|
|
45
|
+
4. Frame each with 2-3 concrete options so the user can react rather than generate from scratch
|
|
46
|
+
""".strip()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main() -> int:
|
|
50
|
+
try:
|
|
51
|
+
payload = load_hook_input()
|
|
52
|
+
if not payload:
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
permission_mode = payload.get("permission_mode", "")
|
|
56
|
+
if permission_mode != "plan":
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
session_id = str(payload.get("session_id", ""))
|
|
60
|
+
if not session_id:
|
|
61
|
+
log_debug("plan_questions_early", "No session_id, skipping")
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
if was_questions_asked(session_id):
|
|
65
|
+
log_debug("plan_questions_early", "Questions already asked, skipping")
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
log_info("plan_questions_early", "Plan mode detected, injecting Phase A prompt")
|
|
69
|
+
print(f"<system-reminder>{PHASE_A_PROMPT}</system-reminder>")
|
|
70
|
+
|
|
71
|
+
except Exception as e:
|
|
72
|
+
from base.hook_utils import log_hook_error
|
|
73
|
+
log_hook_error("plan_questions_early", e, "UserPromptSubmit")
|
|
74
|
+
log_error("plan_questions_early", str(e))
|
|
75
|
+
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
from base.hook_utils import run_hook
|
|
81
|
+
run_hook(main, "plan_questions_early")
|
|
@@ -29,12 +29,16 @@ import tempfile
|
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
from typing import Any, Dict
|
|
31
31
|
|
|
32
|
-
# Add lib
|
|
32
|
+
# Add lib directories to path for imports
|
|
33
33
|
_hook_dir = Path(__file__).resolve().parent
|
|
34
34
|
_lib_dir = _hook_dir.parent / "lib"
|
|
35
|
+
_shared_lib = _hook_dir.parent.parent / "_shared" / "lib"
|
|
35
36
|
sys.path.insert(0, str(_lib_dir))
|
|
37
|
+
sys.path.insert(0, str(_shared_lib))
|
|
36
38
|
|
|
37
|
-
from
|
|
39
|
+
from base.hook_utils import emit_context
|
|
40
|
+
from base.logger import log_debug, log_info, log_warn, log_error
|
|
41
|
+
from utils import sanitize_filename
|
|
38
42
|
|
|
39
43
|
|
|
40
44
|
# ---------------------------
|
|
@@ -78,7 +82,7 @@ def load_config(project_dir: Path) -> Dict[str, Any]:
|
|
|
78
82
|
section = full_config.get("stuckDetection", {})
|
|
79
83
|
return {**DEFAULT_CONFIG, **section}
|
|
80
84
|
except Exception as e:
|
|
81
|
-
|
|
85
|
+
log_warn("suggest-fresh-perspective", f"Failed to load config: {e}")
|
|
82
86
|
return DEFAULT_CONFIG.copy()
|
|
83
87
|
|
|
84
88
|
|
|
@@ -144,7 +148,7 @@ def save_state(session_id: str, state: Dict[str, Any]) -> None:
|
|
|
144
148
|
try:
|
|
145
149
|
state_path.write_text(json.dumps(state), encoding="utf-8")
|
|
146
150
|
except Exception as e:
|
|
147
|
-
|
|
151
|
+
log_warn("suggest-fresh-perspective", f"Failed to save state: {e}")
|
|
148
152
|
|
|
149
153
|
|
|
150
154
|
# ---------------------------
|
|
@@ -226,19 +230,15 @@ def should_suggest(state: Dict[str, Any], cooldown: int) -> bool:
|
|
|
226
230
|
return state.get("tool_calls_since_suggestion", 0) >= cooldown
|
|
227
231
|
|
|
228
232
|
|
|
229
|
-
def create_suggestion() ->
|
|
230
|
-
"""
|
|
231
|
-
|
|
232
|
-
"
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
"---\n"
|
|
239
|
-
)
|
|
240
|
-
}
|
|
241
|
-
}
|
|
233
|
+
def create_suggestion() -> None:
|
|
234
|
+
"""Emit the suggestion via hook utility."""
|
|
235
|
+
emit_context(
|
|
236
|
+
"\n---\n"
|
|
237
|
+
"**Stuck?** You've been working on similar issues for a while. "
|
|
238
|
+
"Consider running `/fresh-perspective` to get an unbiased view of the problem "
|
|
239
|
+
"without code context anchoring your thinking.\n"
|
|
240
|
+
"---\n"
|
|
241
|
+
)
|
|
242
242
|
|
|
243
243
|
|
|
244
244
|
def main() -> int:
|
|
@@ -249,8 +249,8 @@ def main() -> int:
|
|
|
249
249
|
except json.JSONDecodeError:
|
|
250
250
|
return 0 # Fail-safe
|
|
251
251
|
|
|
252
|
-
# 1. Check
|
|
253
|
-
if payload.get("
|
|
252
|
+
# 1. Check hook_event_name (cheap dict lookup)
|
|
253
|
+
if payload.get("hook_event_name") != "PostToolUse":
|
|
254
254
|
return 0
|
|
255
255
|
|
|
256
256
|
# 2. Check session_id exists (cheap dict lookup)
|
|
@@ -314,11 +314,11 @@ def main() -> int:
|
|
|
314
314
|
|
|
315
315
|
if is_stuck:
|
|
316
316
|
if error_detected:
|
|
317
|
-
|
|
317
|
+
log_info("suggest-fresh-perspective", "Detected repeated error pattern")
|
|
318
318
|
if file_edit_detected:
|
|
319
|
-
|
|
319
|
+
log_info("suggest-fresh-perspective", "Detected repeated file edits")
|
|
320
320
|
if test_failure_detected:
|
|
321
|
-
|
|
321
|
+
log_info("suggest-fresh-perspective", "Detected repeated test failures")
|
|
322
322
|
|
|
323
323
|
# Only suggest if stuck AND past cooldown
|
|
324
324
|
if is_stuck and should_suggest(state, cooldown):
|
|
@@ -329,11 +329,12 @@ def main() -> int:
|
|
|
329
329
|
|
|
330
330
|
# Only suggest up to maxSuggestions times per session
|
|
331
331
|
if state["suggestion_count"] <= max_suggestions:
|
|
332
|
-
|
|
333
|
-
|
|
332
|
+
log_info("suggest-fresh-perspective", f"Suggesting fresh perspective (suggestion #{state['suggestion_count']})")
|
|
333
|
+
create_suggestion()
|
|
334
334
|
|
|
335
335
|
return 0
|
|
336
336
|
|
|
337
337
|
|
|
338
338
|
if __name__ == "__main__":
|
|
339
|
-
|
|
339
|
+
from base.hook_utils import run_hook
|
|
340
|
+
run_hook(main, "suggest_fresh_perspective")
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
| `state.py` | Plan state file management and iteration tracking |
|
|
13
13
|
| `orchestrator.py` | Plan complexity analysis and agent selection |
|
|
14
14
|
| `reviewers/` | Plan review implementations (package — see below) |
|
|
15
|
-
| `atomic_write.py` | Atomic file writes for crash safety |
|
|
16
15
|
| `constants.py` | Shared constants and feature flags (e.g., `ENABLE_ROBUST_PLAN_WRITES`) |
|
|
17
16
|
| `debug.py` | Permanent debug logging to context folder (`CCNATIVE_DEBUG_DISABLE=1` to disable) |
|
|
18
17
|
| `__init__.py` | Package exports |
|
|
@@ -35,8 +34,8 @@
|
|
|
35
34
|
Hooks (cc-native-plan-review.py, etc.)
|
|
36
35
|
│
|
|
37
36
|
├── lib/utils.py (core utilities)
|
|
38
|
-
│ └── lib/atomic_write.py
|
|
39
37
|
│ └── lib/constants.py
|
|
38
|
+
│ └── _shared/lib/base/atomic_write.py
|
|
40
39
|
│
|
|
41
40
|
├── lib/state.py (state management)
|
|
42
41
|
│ └── lib/utils.py (eprint)
|
|
@@ -135,7 +134,8 @@ This is a recurring issue. Any path string comparison must handle both separator
|
|
|
135
134
|
For critical files (state, reviews), use atomic writes to prevent corruption on crash:
|
|
136
135
|
|
|
137
136
|
```python
|
|
138
|
-
from
|
|
137
|
+
# Import from shared lib (canonical location)
|
|
138
|
+
from _shared.lib.base.atomic_write import atomic_write
|
|
139
139
|
|
|
140
140
|
# CORRECT - atomic write
|
|
141
141
|
success, error = atomic_write(path, content)
|
|
@@ -245,7 +245,7 @@ content = path.read_text() # May use cp1252 on Windows
|
|
|
245
245
|
These are reminders based on past issues. Not enforcement rules.
|
|
246
246
|
|
|
247
247
|
- **Don't import from `_cc-native/lib/` in `_shared/lib/`** - wrong direction, creates circular deps
|
|
248
|
-
- **Don't use `print()` for debugging** - use `
|
|
248
|
+
- **Don't use `print()` for debugging** - use `log_debug/log_info/log_warn/log_error` from `_shared/lib/base/logger.py` (writes to stderr + `_output/hook-log.jsonl`)
|
|
249
249
|
- **Don't modify data class fields** without updating all consumers (hooks, formatters, tests)
|
|
250
250
|
- **Don't hardcode paths** - use `Path(__file__)`, env vars, or config
|
|
251
251
|
- **Don't forget `encoding="utf-8"`** on file operations - Windows defaults are unsafe
|
|
@@ -260,4 +260,6 @@ These are reminders based on past issues. Not enforcement rules.
|
|
|
260
260
|
|
|
261
261
|
| Date | Change |
|
|
262
262
|
|------|--------|
|
|
263
|
+
| 2026-02-07 | Unified logger: all diagnostic logging uses `_shared/lib/base/logger.py` instead of eprint/print-to-stderr |
|
|
264
|
+
| 2026-02-06 | Remove duplicate `atomic_write.py` — consolidated to `_shared/lib/base/atomic_write.py` |
|
|
263
265
|
| 2026-02-03 | Initial creation |
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,20 +1,35 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Permanent debug logging for cc-native hooks.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
Thin delegation layer over the unified logger (_shared/lib/base/logger.py).
|
|
5
|
+
Logs are written to context folder: _output/contexts/<context-id>/debug/hook-log.jsonl
|
|
5
6
|
Append-only, cleaned up when context is archived.
|
|
6
7
|
Can be disabled via CCNATIVE_DEBUG_DISABLE=1 environment variable.
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
|
-
import json
|
|
10
10
|
import os
|
|
11
|
-
from datetime import datetime
|
|
12
11
|
from pathlib import Path
|
|
13
12
|
from typing import Any, Optional
|
|
14
13
|
|
|
15
14
|
# Feature flag - set CCNATIVE_DEBUG_DISABLE=1 to turn off
|
|
16
15
|
DEBUG_ENABLED = os.environ.get("CCNATIVE_DEBUG_DISABLE", "").lower() not in ("1", "true", "yes")
|
|
17
16
|
|
|
17
|
+
# Import unified logger
|
|
18
|
+
try:
|
|
19
|
+
from _shared.lib.base.logger import hook_log
|
|
20
|
+
except ImportError:
|
|
21
|
+
# Fallback: try relative import path used by hooks
|
|
22
|
+
try:
|
|
23
|
+
import sys
|
|
24
|
+
_shared = Path(__file__).parent.parent.parent.parent / "_shared"
|
|
25
|
+
if str(_shared) not in sys.path:
|
|
26
|
+
sys.path.insert(0, str(_shared))
|
|
27
|
+
from lib.base.logger import hook_log
|
|
28
|
+
except ImportError:
|
|
29
|
+
# Last resort: no-op
|
|
30
|
+
def hook_log(*args, **kwargs):
|
|
31
|
+
pass
|
|
32
|
+
|
|
18
33
|
|
|
19
34
|
def get_debug_dir(context_path: Path) -> Path:
|
|
20
35
|
"""Get or create debug directory within context folder.
|
|
@@ -51,7 +66,7 @@ def debug_log(
|
|
|
51
66
|
message: str,
|
|
52
67
|
data: Optional[Any] = None
|
|
53
68
|
) -> None:
|
|
54
|
-
"""Write a debug log entry
|
|
69
|
+
"""Write a debug log entry. Delegates to unified logger.
|
|
55
70
|
|
|
56
71
|
Args:
|
|
57
72
|
context_path: Path to context folder
|
|
@@ -63,22 +78,15 @@ def debug_log(
|
|
|
63
78
|
if not DEBUG_ENABLED:
|
|
64
79
|
return
|
|
65
80
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
except Exception:
|
|
76
|
-
entry += f"\n<data serialization failed: {type(data)}>"
|
|
77
|
-
|
|
78
|
-
with open(log_path, "a", encoding="utf-8") as f:
|
|
79
|
-
f.write(entry + "\n\n")
|
|
80
|
-
except Exception:
|
|
81
|
-
pass # Never fail on debug logging
|
|
81
|
+
hook_log(
|
|
82
|
+
"debug",
|
|
83
|
+
session_name,
|
|
84
|
+
message,
|
|
85
|
+
component=component,
|
|
86
|
+
data=data,
|
|
87
|
+
context_path=context_path,
|
|
88
|
+
stderr=False,
|
|
89
|
+
)
|
|
82
90
|
|
|
83
91
|
|
|
84
92
|
def debug_raw(
|
|
@@ -89,7 +97,7 @@ def debug_raw(
|
|
|
89
97
|
raw: str,
|
|
90
98
|
max_len: int = 10000
|
|
91
99
|
) -> None:
|
|
92
|
-
"""Log raw output (stdout, stderr, etc).
|
|
100
|
+
"""Log raw output (stdout, stderr, etc). Delegates to unified logger.
|
|
93
101
|
|
|
94
102
|
Args:
|
|
95
103
|
context_path: Path to context folder
|
|
@@ -104,7 +112,14 @@ def debug_raw(
|
|
|
104
112
|
|
|
105
113
|
truncated = raw[:max_len] if len(raw) > max_len else raw
|
|
106
114
|
suffix = f" [TRUNCATED from {len(raw)} chars]" if len(raw) > max_len else ""
|
|
107
|
-
|
|
115
|
+
hook_log(
|
|
116
|
+
"debug",
|
|
117
|
+
session_name,
|
|
118
|
+
f"{label}{suffix}: {truncated}",
|
|
119
|
+
component=component,
|
|
120
|
+
context_path=context_path,
|
|
121
|
+
stderr=False,
|
|
122
|
+
)
|
|
108
123
|
|
|
109
124
|
|
|
110
125
|
def cleanup_debug_folder(context_path: Path) -> None:
|
|
@@ -15,9 +15,14 @@ from typing import Any, Dict, List, Optional
|
|
|
15
15
|
_lib_dir = Path(__file__).resolve().parent
|
|
16
16
|
sys.path.insert(0, str(_lib_dir))
|
|
17
17
|
|
|
18
|
-
from utils import OrchestratorResult,
|
|
18
|
+
from utils import OrchestratorResult, parse_json_maybe
|
|
19
19
|
from reviewers.base import AgentConfig, OrchestratorConfig
|
|
20
20
|
|
|
21
|
+
# Import logger
|
|
22
|
+
_shared_logger = Path(__file__).resolve().parent.parent.parent / "_shared" / "lib"
|
|
23
|
+
sys.path.insert(0, str(_shared_logger))
|
|
24
|
+
from base.logger import log_debug, log_info, log_warn, log_error
|
|
25
|
+
|
|
21
26
|
# Import shared subprocess utilities
|
|
22
27
|
_shared_lib = Path(__file__).resolve().parent.parent.parent / "_shared" / "lib" / "base"
|
|
23
28
|
sys.path.insert(0, str(_shared_lib))
|
|
@@ -119,18 +124,18 @@ def _parse_claude_output(raw: str) -> Optional[Dict[str, Any]]:
|
|
|
119
124
|
result = json.loads(raw)
|
|
120
125
|
if isinstance(result, dict):
|
|
121
126
|
if "structured_output" in result:
|
|
122
|
-
|
|
127
|
+
log_debug("orchestrator", "Found structured_output in root dict", component="parse")
|
|
123
128
|
return result["structured_output"]
|
|
124
129
|
if result.get("type") == "assistant":
|
|
125
130
|
message = result.get("message", {})
|
|
126
131
|
content = message.get("content", [])
|
|
127
132
|
for item in content:
|
|
128
133
|
if isinstance(item, dict) and item.get("name") == "StructuredOutput":
|
|
129
|
-
|
|
134
|
+
log_debug("orchestrator", "Found StructuredOutput in assistant message content", component="parse")
|
|
130
135
|
return item.get("input", {})
|
|
131
|
-
|
|
136
|
+
log_debug("orchestrator", "Assistant message found but no StructuredOutput tool use in content", component="parse")
|
|
132
137
|
elif isinstance(result, list):
|
|
133
|
-
|
|
138
|
+
log_debug("orchestrator", f"Received list of {len(result)} events, searching for assistant message", component="parse")
|
|
134
139
|
for i, event in enumerate(result):
|
|
135
140
|
if not isinstance(event, dict):
|
|
136
141
|
continue
|
|
@@ -139,16 +144,16 @@ def _parse_claude_output(raw: str) -> Optional[Dict[str, Any]]:
|
|
|
139
144
|
content = message.get("content", [])
|
|
140
145
|
for item in content:
|
|
141
146
|
if isinstance(item, dict) and item.get("name") == "StructuredOutput":
|
|
142
|
-
|
|
147
|
+
log_debug("orchestrator", f"Found StructuredOutput in event[{i}] assistant message", component="parse")
|
|
143
148
|
return item.get("input", {})
|
|
144
|
-
|
|
149
|
+
log_debug("orchestrator", "No StructuredOutput found in any assistant message in event list", component="parse")
|
|
145
150
|
except json.JSONDecodeError as e:
|
|
146
|
-
|
|
151
|
+
log_warn("orchestrator", f"JSON decode error: {e}", component="parse")
|
|
147
152
|
except Exception as e:
|
|
148
|
-
|
|
153
|
+
log_error("orchestrator", f"Unexpected error during structured parsing: {e}", component="parse")
|
|
149
154
|
|
|
150
155
|
# Fallback to heuristic extraction
|
|
151
|
-
|
|
156
|
+
log_debug("orchestrator", "No structured output found, falling back to heuristic JSON extraction", component="parse")
|
|
152
157
|
return parse_json_maybe(raw)
|
|
153
158
|
|
|
154
159
|
|
|
@@ -175,7 +180,7 @@ def run_orchestrator(
|
|
|
175
180
|
Returns:
|
|
176
181
|
OrchestratorResult with complexity, category, and selected agents
|
|
177
182
|
"""
|
|
178
|
-
|
|
183
|
+
log_info("orchestrator", "Starting plan analysis...")
|
|
179
184
|
|
|
180
185
|
if mandatory_names is None:
|
|
181
186
|
mandatory_names = set()
|
|
@@ -188,12 +193,12 @@ def run_orchestrator(
|
|
|
188
193
|
non_mandatory = [a for a in agent_library if a.enabled and a.name not in mandatory_names]
|
|
189
194
|
valid_names = [a.name for a in non_mandatory]
|
|
190
195
|
|
|
191
|
-
|
|
192
|
-
|
|
196
|
+
log_debug("orchestrator", f"Mandatory agents (always run): {sorted(mandatory_names)}")
|
|
197
|
+
log_debug("orchestrator", f"Non-mandatory agents for selection: {valid_names}")
|
|
193
198
|
|
|
194
199
|
claude_path = shutil.which("claude")
|
|
195
200
|
if claude_path is None:
|
|
196
|
-
|
|
201
|
+
log_warn("orchestrator", "Claude CLI not found on PATH, falling back to medium complexity")
|
|
197
202
|
return OrchestratorResult(
|
|
198
203
|
complexity="medium",
|
|
199
204
|
category="code",
|
|
@@ -202,7 +207,7 @@ def run_orchestrator(
|
|
|
202
207
|
error="claude CLI not found on PATH",
|
|
203
208
|
)
|
|
204
209
|
|
|
205
|
-
|
|
210
|
+
log_debug("orchestrator", f"Found Claude CLI at: {claude_path}")
|
|
206
211
|
|
|
207
212
|
# Build agent list from non-mandatory agents only
|
|
208
213
|
agent_list = "\n".join([
|
|
@@ -266,7 +271,7 @@ Call StructuredOutput now with: complexity, category, selectedAgents, reasoning"
|
|
|
266
271
|
"--system-prompt", system_prompt,
|
|
267
272
|
]
|
|
268
273
|
|
|
269
|
-
|
|
274
|
+
log_info("orchestrator", f"Running with model: {config.model}, timeout: {config.timeout}s")
|
|
270
275
|
|
|
271
276
|
# Get environment for internal subprocess (bypasses hooks)
|
|
272
277
|
env = get_internal_subprocess_env()
|
|
@@ -283,7 +288,7 @@ Call StructuredOutput now with: complexity, category, selectedAgents, reasoning"
|
|
|
283
288
|
env=env,
|
|
284
289
|
)
|
|
285
290
|
except subprocess.TimeoutExpired:
|
|
286
|
-
|
|
291
|
+
log_warn("orchestrator", f"TIMEOUT after {config.timeout}s, falling back to medium complexity")
|
|
287
292
|
return OrchestratorResult(
|
|
288
293
|
complexity="medium",
|
|
289
294
|
category="code",
|
|
@@ -292,7 +297,7 @@ Call StructuredOutput now with: complexity, category, selectedAgents, reasoning"
|
|
|
292
297
|
error=f"Orchestrator timed out after {config.timeout}s",
|
|
293
298
|
)
|
|
294
299
|
except Exception as ex:
|
|
295
|
-
|
|
300
|
+
log_error("orchestrator", f"Exception: {ex}, falling back to medium complexity")
|
|
296
301
|
return OrchestratorResult(
|
|
297
302
|
complexity="medium",
|
|
298
303
|
category="code",
|
|
@@ -301,26 +306,26 @@ Call StructuredOutput now with: complexity, category, selectedAgents, reasoning"
|
|
|
301
306
|
error=str(ex),
|
|
302
307
|
)
|
|
303
308
|
|
|
304
|
-
|
|
309
|
+
log_debug("orchestrator", f"Exit code: {p.returncode}")
|
|
305
310
|
|
|
306
311
|
raw = (p.stdout or "").strip()
|
|
307
312
|
if p.stderr:
|
|
308
|
-
|
|
313
|
+
log_debug("orchestrator", f"stderr: {p.stderr[:300]}")
|
|
309
314
|
|
|
310
315
|
obj = _parse_claude_output(raw)
|
|
311
316
|
|
|
312
317
|
# Debug logging to diagnose empty selectedAgents issue
|
|
313
|
-
|
|
318
|
+
log_debug("orchestrator", f"Raw output length: {len(raw)} chars")
|
|
314
319
|
if raw:
|
|
315
|
-
|
|
316
|
-
|
|
320
|
+
log_debug("orchestrator", f"Raw output (first 500 chars): {raw[:500]}")
|
|
321
|
+
log_debug("orchestrator", f"Parsed obj: {obj}")
|
|
317
322
|
if obj:
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
323
|
+
log_debug("orchestrator", f"obj keys: {list(obj.keys())}")
|
|
324
|
+
log_debug("orchestrator", f"selectedAgents value: {obj.get('selectedAgents', 'MISSING')}")
|
|
325
|
+
log_debug("orchestrator", f"reasoning value: {obj.get('reasoning', 'MISSING')}")
|
|
321
326
|
|
|
322
327
|
if not obj:
|
|
323
|
-
|
|
328
|
+
log_warn("orchestrator", "Failed to parse output, falling back to medium complexity")
|
|
324
329
|
return OrchestratorResult(
|
|
325
330
|
complexity="medium",
|
|
326
331
|
category="code",
|
|
@@ -345,8 +350,8 @@ Call StructuredOutput now with: complexity, category, selectedAgents, reasoning"
|
|
|
345
350
|
reasoning = str(obj.get("reasoning", "")).strip() or "No reasoning provided"
|
|
346
351
|
skip_reason = obj.get("skipReason")
|
|
347
352
|
|
|
348
|
-
|
|
349
|
-
|
|
353
|
+
log_info("orchestrator", f"Result: complexity={complexity}, category={category}, agents={selected_agents}")
|
|
354
|
+
log_debug("orchestrator", f"Reasoning: {reasoning}")
|
|
350
355
|
|
|
351
356
|
return OrchestratorResult(
|
|
352
357
|
complexity=complexity,
|
package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|