aiwcli 0.10.3 → 0.11.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 +1 -1
- package/dist/commands/clear.js +28 -131
- package/dist/commands/init/index.js +3 -3
- package/dist/lib/gitignore-manager.d.ts +32 -0
- package/dist/lib/gitignore-manager.js +141 -2
- package/dist/templates/CLAUDE.md +8 -8
- package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
- package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
- package/dist/templates/_shared/.claude/settings.json +7 -7
- package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
- package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
- package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
- package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
- package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
- package/dist/templates/_shared/hooks-ts/session_end.ts +104 -0
- package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
- package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
- package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
- package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
- package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
- package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
- package/dist/templates/_shared/lib-ts/base/logger.ts +31 -15
- package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +139 -0
- package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
- package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +61 -37
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
- package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
- package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +159 -0
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
- package/dist/templates/_shared/lib-ts/types.ts +68 -55
- package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
- package/dist/templates/_shared/scripts/resume_handoff.ts +321 -0
- package/dist/templates/_shared/scripts/save_handoff.ts +21 -21
- package/dist/templates/_shared/scripts/status_line.ts +733 -0
- package/dist/templates/cc-native/.claude/settings.json +175 -185
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +921 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +157 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +709 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +124 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +119 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +162 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +249 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +155 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +106 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +243 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +310 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -9
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/templates/_shared/hooks/__init__.py +0 -16
- 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 +0 -177
- package/dist/templates/_shared/hooks/context_monitor.py +0 -270
- package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
- package/dist/templates/_shared/hooks/pre_compact.py +0 -104
- package/dist/templates/_shared/hooks/session_end.py +0 -173
- package/dist/templates/_shared/hooks/session_start.py +0 -206
- package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
- package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
- package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
- package/dist/templates/_shared/lib/__init__.py +0 -1
- package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__init__.py +0 -65
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/atomic_write.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__/stop_words.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
- package/dist/templates/_shared/lib/base/constants.py +0 -358
- package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
- package/dist/templates/_shared/lib/base/inference.py +0 -307
- package/dist/templates/_shared/lib/base/logger.py +0 -305
- package/dist/templates/_shared/lib/base/stop_words.py +0 -221
- package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
- package/dist/templates/_shared/lib/base/utils.py +0 -263
- package/dist/templates/_shared/lib/context/__init__.py +0 -102
- 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_extractor.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__/event_log.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_archive.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_sync.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 +0 -317
- package/dist/templates/_shared/lib/context/context_selector.py +0 -508
- package/dist/templates/_shared/lib/context/context_store.py +0 -653
- package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
- package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
- package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
- 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 +0 -278
- package/dist/templates/_shared/lib/templates/README.md +0 -206
- package/dist/templates/_shared/lib/templates/__init__.py +0 -36
- 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__/persona_questions.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/formatters.py +0 -146
- package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
- 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 +0 -357
- package/dist/templates/_shared/scripts/status_line.py +0 -716
- package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/MIGRATION.md +0 -86
- 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 +0 -130
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
- package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.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/constants.py +0 -45
- package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
- 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 +0 -215
- package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
- package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
- package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
- 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 +0 -168
- package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
|
@@ -1,81 +0,0 @@
|
|
|
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")
|
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
PostToolUse hook - suggests /fresh-perspective when user appears stuck.
|
|
4
|
-
|
|
5
|
-
Detection patterns:
|
|
6
|
-
1. Same error appearing 3+ times
|
|
7
|
-
2. Repeated edits to same file without resolution
|
|
8
|
-
3. Test failures after multiple fix attempts
|
|
9
|
-
|
|
10
|
-
Behavior: Suggests (doesn't force) running /fresh-perspective.
|
|
11
|
-
Non-blocking - always returns success.
|
|
12
|
-
|
|
13
|
-
Configuration (in _cc-native/plan-review.config.json):
|
|
14
|
-
"stuckDetection": {
|
|
15
|
-
"enabled": true, // Set to false to disable entirely
|
|
16
|
-
"errorThreshold": 3, // Errors before suggesting
|
|
17
|
-
"fileEditThreshold": 4, // Edits to same file before suggesting
|
|
18
|
-
"testFailureThreshold": 3, // Test failures before suggesting
|
|
19
|
-
"cooldown": 10, // Tool calls between suggestions
|
|
20
|
-
"maxSuggestions": 3 // Max suggestions per session
|
|
21
|
-
}
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
import json
|
|
25
|
-
import os
|
|
26
|
-
import re
|
|
27
|
-
import sys
|
|
28
|
-
import tempfile
|
|
29
|
-
from pathlib import Path
|
|
30
|
-
from typing import Any, Dict
|
|
31
|
-
|
|
32
|
-
# Add lib directories to path for imports
|
|
33
|
-
_hook_dir = Path(__file__).resolve().parent
|
|
34
|
-
_lib_dir = _hook_dir.parent / "lib"
|
|
35
|
-
_shared_lib = _hook_dir.parent.parent / "_shared" / "lib"
|
|
36
|
-
sys.path.insert(0, str(_lib_dir))
|
|
37
|
-
sys.path.insert(0, str(_shared_lib))
|
|
38
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# ---------------------------
|
|
45
|
-
# Configuration (defaults, overridden by config.json)
|
|
46
|
-
# ---------------------------
|
|
47
|
-
|
|
48
|
-
DEFAULT_CONFIG = {
|
|
49
|
-
"enabled": True,
|
|
50
|
-
"errorThreshold": 3,
|
|
51
|
-
"fileEditThreshold": 4,
|
|
52
|
-
"testFailureThreshold": 3,
|
|
53
|
-
"cooldown": 10,
|
|
54
|
-
"maxSuggestions": 3,
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _int_or_default(value: Any, default: int) -> int:
|
|
59
|
-
"""Coerce value to int, return default if not possible.
|
|
60
|
-
|
|
61
|
-
Handles string numbers, floats, and invalid types gracefully.
|
|
62
|
-
"""
|
|
63
|
-
if isinstance(value, int):
|
|
64
|
-
return value
|
|
65
|
-
if isinstance(value, float):
|
|
66
|
-
return int(value)
|
|
67
|
-
if isinstance(value, str):
|
|
68
|
-
try:
|
|
69
|
-
return int(value)
|
|
70
|
-
except ValueError:
|
|
71
|
-
return default
|
|
72
|
-
return default
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def load_config(project_dir: Path) -> Dict[str, Any]:
|
|
76
|
-
"""Load stuckDetection config from _cc-native/plan-review.config.json."""
|
|
77
|
-
config_path = project_dir / "_cc-native" / "plan-review.config.json"
|
|
78
|
-
if not config_path.exists():
|
|
79
|
-
return DEFAULT_CONFIG.copy()
|
|
80
|
-
try:
|
|
81
|
-
full_config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
82
|
-
section = full_config.get("stuckDetection", {})
|
|
83
|
-
return {**DEFAULT_CONFIG, **section}
|
|
84
|
-
except Exception as e:
|
|
85
|
-
log_warn("suggest-fresh-perspective", f"Failed to load config: {e}")
|
|
86
|
-
return DEFAULT_CONFIG.copy()
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def get_project_dir(payload: Dict[str, Any]) -> Path:
|
|
90
|
-
"""Get project directory from payload or environment."""
|
|
91
|
-
p = os.environ.get("CLAUDE_PROJECT_DIR") or payload.get("cwd") or os.getcwd()
|
|
92
|
-
return Path(p)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# ---------------------------
|
|
96
|
-
# Compiled patterns (performance optimization)
|
|
97
|
-
# ---------------------------
|
|
98
|
-
|
|
99
|
-
# Single combined pattern for error detection (case-insensitive)
|
|
100
|
-
_ERROR_PATTERN = re.compile(
|
|
101
|
-
r'(error:|failed|exception)',
|
|
102
|
-
re.IGNORECASE
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
# Combined pattern for test failures
|
|
106
|
-
_TEST_FAILURE_PATTERN = re.compile(
|
|
107
|
-
r'(\d+\s+failed|FAIL\s|✗|AssertionError|test.*failed|npm\s+ERR!.*test)',
|
|
108
|
-
re.IGNORECASE
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
# Pattern for normalizing error messages (line numbers)
|
|
112
|
-
_LINE_NUMBER_PATTERN = re.compile(r':\d+')
|
|
113
|
-
_MULTI_DIGIT_PATTERN = re.compile(r'\d{2,}')
|
|
114
|
-
_PATH_PATTERN = re.compile(r'[/\\][^\s/\\]+[/\\]')
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
# ---------------------------
|
|
118
|
-
# State management (session-scoped)
|
|
119
|
-
# ---------------------------
|
|
120
|
-
|
|
121
|
-
def get_state_path(session_id: str) -> Path:
|
|
122
|
-
"""Get path to stuck-detection state file for this session."""
|
|
123
|
-
safe_id = sanitize_filename(str(session_id), max_len=32)
|
|
124
|
-
return Path(tempfile.gettempdir()) / f"cc-native-stuck-state-{safe_id}.json"
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def load_state(session_id: str) -> Dict[str, Any]:
|
|
128
|
-
"""Load stuck detection state for this session."""
|
|
129
|
-
state_path = get_state_path(session_id)
|
|
130
|
-
default_state = {
|
|
131
|
-
"error_hashes": {}, # hash -> count
|
|
132
|
-
"file_edits": {}, # file_path -> count
|
|
133
|
-
"test_failures": 0,
|
|
134
|
-
"tool_calls_since_suggestion": 0,
|
|
135
|
-
"suggestion_count": 0,
|
|
136
|
-
}
|
|
137
|
-
if not state_path.exists():
|
|
138
|
-
return default_state
|
|
139
|
-
try:
|
|
140
|
-
return json.loads(state_path.read_text(encoding="utf-8"))
|
|
141
|
-
except Exception:
|
|
142
|
-
return default_state
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def save_state(session_id: str, state: Dict[str, Any]) -> None:
|
|
146
|
-
"""Save stuck detection state for this session."""
|
|
147
|
-
state_path = get_state_path(session_id)
|
|
148
|
-
try:
|
|
149
|
-
state_path.write_text(json.dumps(state), encoding="utf-8")
|
|
150
|
-
except Exception as e:
|
|
151
|
-
log_warn("suggest-fresh-perspective", f"Failed to save state: {e}")
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
# ---------------------------
|
|
155
|
-
# Detection logic
|
|
156
|
-
# ---------------------------
|
|
157
|
-
|
|
158
|
-
def hash_error(error_text: str) -> str:
|
|
159
|
-
"""Create a simple hash of an error message for deduplication.
|
|
160
|
-
|
|
161
|
-
Normalizes by removing line numbers and multi-digit numbers,
|
|
162
|
-
but preserves enough context to distinguish different errors.
|
|
163
|
-
"""
|
|
164
|
-
# Normalize: remove line numbers, preserve error type
|
|
165
|
-
normalized = _LINE_NUMBER_PATTERN.sub(':N', error_text)
|
|
166
|
-
normalized = _MULTI_DIGIT_PATTERN.sub('N', normalized)
|
|
167
|
-
# Simplify paths but keep some structure
|
|
168
|
-
normalized = _PATH_PATTERN.sub('.../', normalized)
|
|
169
|
-
# Take first 100 chars after normalization
|
|
170
|
-
return normalized[:100]
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def detect_repeated_error(state: Dict[str, Any], tool_result: str, threshold: int) -> bool:
|
|
174
|
-
"""Check if we're seeing the same error repeatedly.
|
|
175
|
-
|
|
176
|
-
Returns True if threshold reached, always updates state.
|
|
177
|
-
"""
|
|
178
|
-
if not tool_result:
|
|
179
|
-
return False
|
|
180
|
-
|
|
181
|
-
if _ERROR_PATTERN.search(tool_result):
|
|
182
|
-
error_hash = hash_error(tool_result)
|
|
183
|
-
state["error_hashes"][error_hash] = state["error_hashes"].get(error_hash, 0) + 1
|
|
184
|
-
return state["error_hashes"][error_hash] >= threshold
|
|
185
|
-
|
|
186
|
-
return False
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def detect_repeated_file_edits(state: Dict[str, Any], tool_name: str, tool_input: Dict[str, Any], threshold: int) -> bool:
|
|
190
|
-
"""Check if we're editing the same file repeatedly.
|
|
191
|
-
|
|
192
|
-
Returns True if threshold reached, always updates state.
|
|
193
|
-
"""
|
|
194
|
-
if tool_name != "Edit":
|
|
195
|
-
return False
|
|
196
|
-
|
|
197
|
-
# Validate tool_input is a dict
|
|
198
|
-
if not isinstance(tool_input, dict):
|
|
199
|
-
return False
|
|
200
|
-
|
|
201
|
-
file_path = tool_input.get("file_path", "")
|
|
202
|
-
if not file_path:
|
|
203
|
-
return False
|
|
204
|
-
|
|
205
|
-
state["file_edits"][file_path] = state["file_edits"].get(file_path, 0) + 1
|
|
206
|
-
return state["file_edits"][file_path] >= threshold
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def detect_test_failures(state: Dict[str, Any], tool_name: str, tool_result: str, threshold: int) -> bool:
|
|
210
|
-
"""Check for repeated test failures.
|
|
211
|
-
|
|
212
|
-
Returns True if threshold reached, always updates state.
|
|
213
|
-
"""
|
|
214
|
-
if tool_name != "Bash":
|
|
215
|
-
return False
|
|
216
|
-
|
|
217
|
-
if _TEST_FAILURE_PATTERN.search(tool_result):
|
|
218
|
-
state["test_failures"] = state.get("test_failures", 0) + 1
|
|
219
|
-
return state["test_failures"] >= threshold
|
|
220
|
-
|
|
221
|
-
return False
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
# ---------------------------
|
|
225
|
-
# Main hook logic
|
|
226
|
-
# ---------------------------
|
|
227
|
-
|
|
228
|
-
def should_suggest(state: Dict[str, Any], cooldown: int) -> bool:
|
|
229
|
-
"""Check if we're past the cooldown period."""
|
|
230
|
-
return state.get("tool_calls_since_suggestion", 0) >= cooldown
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
def main() -> int:
|
|
245
|
-
# === FAST PATH: Cheap checks first, no I/O ===
|
|
246
|
-
|
|
247
|
-
try:
|
|
248
|
-
payload = json.load(sys.stdin)
|
|
249
|
-
except json.JSONDecodeError:
|
|
250
|
-
return 0 # Fail-safe
|
|
251
|
-
|
|
252
|
-
# 1. Check hook_event_name (cheap dict lookup)
|
|
253
|
-
if payload.get("hook_event_name") != "PostToolUse":
|
|
254
|
-
return 0
|
|
255
|
-
|
|
256
|
-
# 2. Check session_id exists (cheap dict lookup)
|
|
257
|
-
session_id = payload.get("session_id")
|
|
258
|
-
if not session_id:
|
|
259
|
-
return 0
|
|
260
|
-
|
|
261
|
-
# 3. Check tool_name is relevant (cheap dict lookup)
|
|
262
|
-
# We only care about Edit and Bash - skip everything else
|
|
263
|
-
tool_name = payload.get("tool_name", "")
|
|
264
|
-
if tool_name not in ("Edit", "Bash"):
|
|
265
|
-
return 0
|
|
266
|
-
|
|
267
|
-
# === SLOW PATH: Only reached for Edit/Bash tools ===
|
|
268
|
-
|
|
269
|
-
# Load configuration (file I/O)
|
|
270
|
-
project_dir = get_project_dir(payload)
|
|
271
|
-
config = load_config(project_dir)
|
|
272
|
-
|
|
273
|
-
# Check if feature is disabled
|
|
274
|
-
if not config.get("enabled", True):
|
|
275
|
-
return 0
|
|
276
|
-
|
|
277
|
-
tool_input = payload.get("tool_input", {})
|
|
278
|
-
tool_result = payload.get("tool_result", {})
|
|
279
|
-
|
|
280
|
-
# Validate tool_input type
|
|
281
|
-
if not isinstance(tool_input, dict):
|
|
282
|
-
tool_input = {}
|
|
283
|
-
|
|
284
|
-
# Extract result text
|
|
285
|
-
result_text = ""
|
|
286
|
-
if isinstance(tool_result, dict):
|
|
287
|
-
result_text = str(tool_result.get("output", "") or tool_result.get("content", ""))
|
|
288
|
-
elif isinstance(tool_result, str):
|
|
289
|
-
result_text = tool_result
|
|
290
|
-
|
|
291
|
-
# Load state (file I/O)
|
|
292
|
-
state = load_state(session_id)
|
|
293
|
-
|
|
294
|
-
# Increment tool call counter
|
|
295
|
-
state["tool_calls_since_suggestion"] = state.get("tool_calls_since_suggestion", 0) + 1
|
|
296
|
-
|
|
297
|
-
# Get thresholds from config (with type coercion for safety)
|
|
298
|
-
error_threshold = _int_or_default(config.get("errorThreshold"), 3)
|
|
299
|
-
file_edit_threshold = _int_or_default(config.get("fileEditThreshold"), 4)
|
|
300
|
-
test_failure_threshold = _int_or_default(config.get("testFailureThreshold"), 3)
|
|
301
|
-
cooldown = _int_or_default(config.get("cooldown"), 10)
|
|
302
|
-
max_suggestions = _int_or_default(config.get("maxSuggestions"), 3)
|
|
303
|
-
|
|
304
|
-
# Run ALL detections (don't short-circuit - each updates state)
|
|
305
|
-
error_detected = detect_repeated_error(state, result_text, error_threshold)
|
|
306
|
-
file_edit_detected = detect_repeated_file_edits(state, tool_name, tool_input, file_edit_threshold)
|
|
307
|
-
test_failure_detected = detect_test_failures(state, tool_name, result_text, test_failure_threshold)
|
|
308
|
-
|
|
309
|
-
# Save state AFTER all detections have run
|
|
310
|
-
save_state(session_id, state)
|
|
311
|
-
|
|
312
|
-
# Check if any detection triggered
|
|
313
|
-
is_stuck = error_detected or file_edit_detected or test_failure_detected
|
|
314
|
-
|
|
315
|
-
if is_stuck:
|
|
316
|
-
if error_detected:
|
|
317
|
-
log_info("suggest-fresh-perspective", "Detected repeated error pattern")
|
|
318
|
-
if file_edit_detected:
|
|
319
|
-
log_info("suggest-fresh-perspective", "Detected repeated file edits")
|
|
320
|
-
if test_failure_detected:
|
|
321
|
-
log_info("suggest-fresh-perspective", "Detected repeated test failures")
|
|
322
|
-
|
|
323
|
-
# Only suggest if stuck AND past cooldown
|
|
324
|
-
if is_stuck and should_suggest(state, cooldown):
|
|
325
|
-
# Reset cooldown
|
|
326
|
-
state["tool_calls_since_suggestion"] = 0
|
|
327
|
-
state["suggestion_count"] = state.get("suggestion_count", 0) + 1
|
|
328
|
-
save_state(session_id, state)
|
|
329
|
-
|
|
330
|
-
# Only suggest up to maxSuggestions times per session
|
|
331
|
-
if state["suggestion_count"] <= max_suggestions:
|
|
332
|
-
log_info("suggest-fresh-perspective", f"Suggesting fresh perspective (suggestion #{state['suggestion_count']})")
|
|
333
|
-
create_suggestion()
|
|
334
|
-
|
|
335
|
-
return 0
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if __name__ == "__main__":
|
|
339
|
-
from base.hook_utils import run_hook
|
|
340
|
-
run_hook(main, "suggest_fresh_perspective")
|
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
# CC-Native Library Development Guide
|
|
2
|
-
|
|
3
|
-
> **Keep this document updated.** When you solve an issue related to library code, add the solution to the relevant section and log it in the Changelog. This document should grow with discovered patterns and fixes—don't wait to be asked.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Module Overview
|
|
8
|
-
|
|
9
|
-
| Module | Purpose |
|
|
10
|
-
|--------|---------|
|
|
11
|
-
| `utils.py` | Core utilities: eprint, sanitize, JSON parsing, artifact writing |
|
|
12
|
-
| `state.py` | Plan state file management and iteration tracking |
|
|
13
|
-
| `orchestrator.py` | Plan complexity analysis and agent selection |
|
|
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) |
|
|
17
|
-
| `__init__.py` | Package exports |
|
|
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
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## Dependency Graph
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
Hooks (cc-native-plan-review.py, etc.)
|
|
35
|
-
│
|
|
36
|
-
├── lib/utils.py (core utilities)
|
|
37
|
-
│ └── lib/constants.py
|
|
38
|
-
│ └── _shared/lib/base/atomic_write.py
|
|
39
|
-
│
|
|
40
|
-
├── lib/state.py (state management)
|
|
41
|
-
│ └── lib/utils.py (eprint)
|
|
42
|
-
│
|
|
43
|
-
├── lib/orchestrator.py (agent selection)
|
|
44
|
-
│ └── lib/utils.py (ReviewerResult, etc.)
|
|
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
|
-
│
|
|
54
|
-
└── _shared/lib/ (shared across all methods)
|
|
55
|
-
├── lib/base/subprocess_utils.py
|
|
56
|
-
├── lib/base/constants.py
|
|
57
|
-
└── lib/context/context_manager.py
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
**Import direction:** Hooks → cc-native lib → shared lib. Never the reverse.
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
## Key Data Classes
|
|
65
|
-
|
|
66
|
-
### ReviewerResult
|
|
67
|
-
|
|
68
|
-
```python
|
|
69
|
-
@dataclass
|
|
70
|
-
class ReviewerResult:
|
|
71
|
-
name: str # Reviewer name (e.g., "codex", "architect-reviewer")
|
|
72
|
-
ok: bool # True if review completed successfully
|
|
73
|
-
verdict: str # "pass" | "warn" | "fail" | "error" | "skip"
|
|
74
|
-
data: Dict[str, Any] # Structured review data (summary, issues, etc.)
|
|
75
|
-
raw: str # Raw response text
|
|
76
|
-
err: str # Error message if any
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### OrchestratorResult
|
|
80
|
-
|
|
81
|
-
```python
|
|
82
|
-
@dataclass
|
|
83
|
-
class OrchestratorResult:
|
|
84
|
-
complexity: str # "simple" | "medium" | "high"
|
|
85
|
-
category: str # "code" | "infrastructure" | "documentation" | etc.
|
|
86
|
-
selected_agents: List[str] # Agent names to run
|
|
87
|
-
reasoning: str # Why these agents were selected
|
|
88
|
-
skip_reason: Optional[str] # Why review was skipped (if applicable)
|
|
89
|
-
error: Optional[str] # Error message if orchestrator failed
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### CombinedReviewResult
|
|
93
|
-
|
|
94
|
-
```python
|
|
95
|
-
@dataclass
|
|
96
|
-
class CombinedReviewResult:
|
|
97
|
-
plan_hash: str # SHA256 hash (first 16 chars)
|
|
98
|
-
overall_verdict: str # Worst verdict across all reviewers
|
|
99
|
-
cli_reviewers: Dict[str, ReviewerResult] # Codex, Gemini results
|
|
100
|
-
orchestration: Optional[OrchestratorResult]
|
|
101
|
-
agents: Dict[str, ReviewerResult] # Agent review results
|
|
102
|
-
timestamp: str # ISO format
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
## Windows Path Handling
|
|
108
|
-
|
|
109
|
-
Windows uses backslashes in paths. Always normalize when comparing:
|
|
110
|
-
|
|
111
|
-
```python
|
|
112
|
-
# CORRECT - works on Windows and Unix
|
|
113
|
-
if ".claude/plans/" in file_path.replace("\\", "/"):
|
|
114
|
-
# Found a plan file
|
|
115
|
-
|
|
116
|
-
# Also correct - use Path for comparisons
|
|
117
|
-
from pathlib import Path
|
|
118
|
-
if Path(".claude/plans") in Path(file_path).parents:
|
|
119
|
-
# Found a plan file
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
```python
|
|
123
|
-
# WRONG - fails on Windows
|
|
124
|
-
if ".claude/plans/" in file_path: # Windows path: ".claude\\plans\\"
|
|
125
|
-
# Never matches on Windows!
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
This is a recurring issue. Any path string comparison must handle both separators.
|
|
129
|
-
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
## Atomic Writes
|
|
133
|
-
|
|
134
|
-
For critical files (state, reviews), use atomic writes to prevent corruption on crash:
|
|
135
|
-
|
|
136
|
-
```python
|
|
137
|
-
# Import from shared lib (canonical location)
|
|
138
|
-
from _shared.lib.base.atomic_write import atomic_write
|
|
139
|
-
|
|
140
|
-
# CORRECT - atomic write
|
|
141
|
-
success, error = atomic_write(path, content)
|
|
142
|
-
if not success:
|
|
143
|
-
eprint(f"[module] Write failed: {error}")
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
```python
|
|
147
|
-
# RISKY - can leave partial file on crash
|
|
148
|
-
path.write_text(content, encoding="utf-8")
|
|
149
|
-
```
|
|
150
|
-
|
|
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.
|
|
152
|
-
|
|
153
|
-
---
|
|
154
|
-
|
|
155
|
-
## Adding New Reviewers
|
|
156
|
-
|
|
157
|
-
1. **Create reviewer file** in `reviewers/` package (e.g., `reviewers/myreviewer.py`):
|
|
158
|
-
```python
|
|
159
|
-
from .base import ReviewerResult, REVIEW_SCHEMA
|
|
160
|
-
|
|
161
|
-
def run_myreviewer_review(
|
|
162
|
-
plan: str,
|
|
163
|
-
schema: Dict[str, Any],
|
|
164
|
-
settings: Dict[str, Any],
|
|
165
|
-
) -> ReviewerResult:
|
|
166
|
-
# Implementation
|
|
167
|
-
return ReviewerResult(
|
|
168
|
-
name="myreviewer",
|
|
169
|
-
ok=True,
|
|
170
|
-
verdict="pass",
|
|
171
|
-
data=parsed_data,
|
|
172
|
-
raw=raw_response,
|
|
173
|
-
err="",
|
|
174
|
-
)
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
2. **Export in `reviewers/__init__.py`**:
|
|
178
|
-
```python
|
|
179
|
-
from .myreviewer import run_myreviewer_review
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
3. **Add config** in `plan-review.config.json`:
|
|
183
|
-
```json
|
|
184
|
-
{
|
|
185
|
-
"planReview": {
|
|
186
|
-
"reviewers": {
|
|
187
|
-
"myreviewer": {"enabled": true, "timeout": 120}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
4. **Wire in hook** (`cc-native-plan-review.py`):
|
|
194
|
-
```python
|
|
195
|
-
from reviewers import run_myreviewer_review
|
|
196
|
-
|
|
197
|
-
if myreviewer_enabled:
|
|
198
|
-
phase1_tasks.append(("myreviewer", lambda: run_myreviewer_review(...)))
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
---
|
|
202
|
-
|
|
203
|
-
## JSON Parsing
|
|
204
|
-
|
|
205
|
-
Use `parse_json_maybe` for LLM responses - it handles markdown code blocks and extraction:
|
|
206
|
-
|
|
207
|
-
```python
|
|
208
|
-
from utils import parse_json_maybe, coerce_to_review
|
|
209
|
-
|
|
210
|
-
# Parse with field validation
|
|
211
|
-
obj = parse_json_maybe(raw_response, require_fields=["verdict", "summary"])
|
|
212
|
-
|
|
213
|
-
# Normalize to expected structure
|
|
214
|
-
ok, verdict, data = coerce_to_review(obj)
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
The parser tries:
|
|
218
|
-
1. Strict JSON parse
|
|
219
|
-
2. Extract `{...}` block from text (handles ```json blocks)
|
|
220
|
-
|
|
221
|
-
---
|
|
222
|
-
|
|
223
|
-
## Encoding
|
|
224
|
-
|
|
225
|
-
Always specify encoding on file operations:
|
|
226
|
-
|
|
227
|
-
```python
|
|
228
|
-
# CORRECT
|
|
229
|
-
content = path.read_text(encoding="utf-8")
|
|
230
|
-
path.write_text(content, encoding="utf-8")
|
|
231
|
-
|
|
232
|
-
with open(path, "r", encoding="utf-8") as f:
|
|
233
|
-
data = json.load(f)
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
```python
|
|
237
|
-
# WRONG - uses system default (can fail on Windows)
|
|
238
|
-
content = path.read_text() # May use cp1252 on Windows
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
---
|
|
242
|
-
|
|
243
|
-
## DO NOT
|
|
244
|
-
|
|
245
|
-
These are reminders based on past issues. Not enforcement rules.
|
|
246
|
-
|
|
247
|
-
- **Don't import from `_cc-native/lib/` in `_shared/lib/`** - wrong direction, creates circular deps
|
|
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
|
-
- **Don't modify data class fields** without updating all consumers (hooks, formatters, tests)
|
|
250
|
-
- **Don't hardcode paths** - use `Path(__file__)`, env vars, or config
|
|
251
|
-
- **Don't forget `encoding="utf-8"`** on file operations - Windows defaults are unsafe
|
|
252
|
-
- **Don't assume forward slashes** in file paths - Windows uses backslashes
|
|
253
|
-
- **Don't skip atomic writes** for critical state files - use `atomic_write` function
|
|
254
|
-
|
|
255
|
-
---
|
|
256
|
-
|
|
257
|
-
## Changelog
|
|
258
|
-
|
|
259
|
-
<!-- Add dated entries as new issues are discovered -->
|
|
260
|
-
|
|
261
|
-
| Date | Change |
|
|
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` |
|
|
265
|
-
| 2026-02-03 | Initial creation |
|