aiwcli 0.9.7 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/run.js +5 -2
- package/dist/lib/claude-settings-types.d.ts +2 -0
- package/dist/templates/CLAUDE.md +49 -18
- package/dist/templates/_shared/.claude/settings.json +4 -0
- package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/archive_plan.py +87 -178
- package/dist/templates/_shared/hooks/context_monitor.py +128 -194
- package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
- package/dist/templates/_shared/hooks/pre_compact.py +104 -0
- package/dist/templates/_shared/hooks/session_end.py +154 -0
- package/dist/templates/_shared/hooks/session_start.py +145 -59
- package/dist/templates/_shared/hooks/task_create_capture.py +26 -49
- package/dist/templates/_shared/hooks/task_update_capture.py +42 -100
- package/dist/templates/_shared/hooks/user_prompt_submit.py +63 -77
- package/dist/templates/_shared/lib/base/__init__.py +16 -0
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/constants.py +18 -4
- package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
- package/dist/templates/_shared/lib/base/inference.py +121 -0
- package/dist/templates/_shared/lib/base/logger.py +291 -0
- package/dist/templates/_shared/lib/base/utils.py +49 -11
- package/dist/templates/_shared/lib/context/__init__.py +72 -80
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
- package/dist/templates/_shared/lib/context/context_selector.py +491 -0
- package/dist/templates/_shared/lib/context/context_store.py +636 -0
- package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
- package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
- package/dist/templates/_shared/lib/templates/README.md +5 -13
- package/dist/templates/_shared/lib/templates/__init__.py +2 -6
- package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +25 -79
- package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.py +39 -19
- package/dist/templates/_shared/scripts/status_line.py +701 -0
- package/dist/templates/_shared/workflows/handoff.md +9 -3
- package/dist/templates/cc-native/.claude/settings.json +64 -9
- package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
- package/dist/templates/cc-native/MIGRATION.md +1 -1
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -1
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +57 -22
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -57
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +208 -158
- package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +35 -10
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +103 -42
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
- package/dist/templates/cc-native/_cc-native/lib/utils.py +210 -43
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
- package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -205
- package/dist/templates/_shared/lib/context/cache.py +0 -444
- package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
- package/dist/templates/_shared/lib/context/context_manager.py +0 -1054
- package/dist/templates/_shared/lib/context/discovery.py +0 -444
- package/dist/templates/_shared/lib/context/event_log.py +0 -308
- package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
- package/dist/templates/_shared/lib/context/task_sync.py +0 -290
- package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
- package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +0 -98
|
@@ -0,0 +1,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")
|
|
@@ -11,12 +11,21 @@
|
|
|
11
11
|
| `utils.py` | Core utilities: eprint, sanitize, JSON parsing, artifact writing |
|
|
12
12
|
| `state.py` | Plan state file management and iteration tracking |
|
|
13
13
|
| `orchestrator.py` | Plan complexity analysis and agent selection |
|
|
14
|
-
| `reviewers
|
|
15
|
-
| `
|
|
16
|
-
| `
|
|
17
|
-
| `async_archive.py` | Async plan archival operations |
|
|
14
|
+
| `reviewers/` | Plan review implementations (package — see below) |
|
|
15
|
+
| `constants.py` | Shared constants and feature flags (e.g., `ENABLE_ROBUST_PLAN_WRITES`) |
|
|
16
|
+
| `debug.py` | Permanent debug logging to context folder (`CCNATIVE_DEBUG_DISABLE=1` to disable) |
|
|
18
17
|
| `__init__.py` | Package exports |
|
|
19
18
|
|
|
19
|
+
### reviewers/ Package
|
|
20
|
+
|
|
21
|
+
| File | Purpose |
|
|
22
|
+
|------|---------|
|
|
23
|
+
| `__init__.py` | Re-exports: `ReviewerResult`, `run_codex_review`, `run_gemini_review`, `run_agent_review` |
|
|
24
|
+
| `base.py` | `ReviewerResult`, `REVIEW_SCHEMA`, `AgentConfig`, `OrchestratorConfig` |
|
|
25
|
+
| `agent.py` | Claude Code agent-based reviewer (uses `--system-prompt`) |
|
|
26
|
+
| `codex.py` | Codex CLI reviewer |
|
|
27
|
+
| `gemini.py` | Google Gemini API reviewer |
|
|
28
|
+
|
|
20
29
|
---
|
|
21
30
|
|
|
22
31
|
## Dependency Graph
|
|
@@ -25,8 +34,8 @@
|
|
|
25
34
|
Hooks (cc-native-plan-review.py, etc.)
|
|
26
35
|
│
|
|
27
36
|
├── lib/utils.py (core utilities)
|
|
28
|
-
│ └── lib/atomic_write.py
|
|
29
37
|
│ └── lib/constants.py
|
|
38
|
+
│ └── _shared/lib/base/atomic_write.py
|
|
30
39
|
│
|
|
31
40
|
├── lib/state.py (state management)
|
|
32
41
|
│ └── lib/utils.py (eprint)
|
|
@@ -34,6 +43,14 @@ Hooks (cc-native-plan-review.py, etc.)
|
|
|
34
43
|
├── lib/orchestrator.py (agent selection)
|
|
35
44
|
│ └── lib/utils.py (ReviewerResult, etc.)
|
|
36
45
|
│
|
|
46
|
+
├── lib/reviewers/ (plan review package)
|
|
47
|
+
│ ├── base.py (ReviewerResult, AgentConfig, schemas)
|
|
48
|
+
│ ├── agent.py → base.py
|
|
49
|
+
│ ├── codex.py → base.py
|
|
50
|
+
│ └── gemini.py → base.py
|
|
51
|
+
│
|
|
52
|
+
├── lib/debug.py (context-folder debug logging)
|
|
53
|
+
│
|
|
37
54
|
└── _shared/lib/ (shared across all methods)
|
|
38
55
|
├── lib/base/subprocess_utils.py
|
|
39
56
|
├── lib/base/constants.py
|
|
@@ -117,7 +134,8 @@ This is a recurring issue. Any path string comparison must handle both separator
|
|
|
117
134
|
For critical files (state, reviews), use atomic writes to prevent corruption on crash:
|
|
118
135
|
|
|
119
136
|
```python
|
|
120
|
-
from
|
|
137
|
+
# Import from shared lib (canonical location)
|
|
138
|
+
from _shared.lib.base.atomic_write import atomic_write
|
|
121
139
|
|
|
122
140
|
# CORRECT - atomic write
|
|
123
141
|
success, error = atomic_write(path, content)
|
|
@@ -130,14 +148,16 @@ if not success:
|
|
|
130
148
|
path.write_text(content, encoding="utf-8")
|
|
131
149
|
```
|
|
132
150
|
|
|
133
|
-
Atomic writes use a temp file + rename pattern.
|
|
151
|
+
Atomic writes use a temp file + rename pattern. The `constants.ENABLE_ROBUST_PLAN_WRITES` feature flag (env: `CC_NATIVE_ROBUST_WRITES`, default: `true`) controls whether atomic writes are used for plan state files.
|
|
134
152
|
|
|
135
153
|
---
|
|
136
154
|
|
|
137
155
|
## Adding New Reviewers
|
|
138
156
|
|
|
139
|
-
1. **Create reviewer
|
|
157
|
+
1. **Create reviewer file** in `reviewers/` package (e.g., `reviewers/myreviewer.py`):
|
|
140
158
|
```python
|
|
159
|
+
from .base import ReviewerResult, REVIEW_SCHEMA
|
|
160
|
+
|
|
141
161
|
def run_myreviewer_review(
|
|
142
162
|
plan: str,
|
|
143
163
|
schema: Dict[str, Any],
|
|
@@ -154,7 +174,10 @@ Atomic writes use a temp file + rename pattern. Check `constants.ENABLE_ROBUST_P
|
|
|
154
174
|
)
|
|
155
175
|
```
|
|
156
176
|
|
|
157
|
-
2. **Export in `__init__.py
|
|
177
|
+
2. **Export in `reviewers/__init__.py`**:
|
|
178
|
+
```python
|
|
179
|
+
from .myreviewer import run_myreviewer_review
|
|
180
|
+
```
|
|
158
181
|
|
|
159
182
|
3. **Add config** in `plan-review.config.json`:
|
|
160
183
|
```json
|
|
@@ -222,7 +245,7 @@ content = path.read_text() # May use cp1252 on Windows
|
|
|
222
245
|
These are reminders based on past issues. Not enforcement rules.
|
|
223
246
|
|
|
224
247
|
- **Don't import from `_cc-native/lib/` in `_shared/lib/`** - wrong direction, creates circular deps
|
|
225
|
-
- **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`)
|
|
226
249
|
- **Don't modify data class fields** without updating all consumers (hooks, formatters, tests)
|
|
227
250
|
- **Don't hardcode paths** - use `Path(__file__)`, env vars, or config
|
|
228
251
|
- **Don't forget `encoding="utf-8"`** on file operations - Windows defaults are unsafe
|
|
@@ -237,4 +260,6 @@ These are reminders based on past issues. Not enforcement rules.
|
|
|
237
260
|
|
|
238
261
|
| Date | Change |
|
|
239
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` |
|
|
240
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:
|