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
|
@@ -22,13 +22,25 @@ from pathlib import Path
|
|
|
22
22
|
from typing import Any, Dict, List, Optional, Tuple
|
|
23
23
|
|
|
24
24
|
try:
|
|
25
|
-
from .atomic_write import atomic_write
|
|
26
25
|
from .constants import ENABLE_ROBUST_PLAN_WRITES
|
|
27
26
|
except ImportError:
|
|
28
27
|
# When imported directly via sys.path (not as a package)
|
|
29
|
-
from atomic_write import atomic_write
|
|
30
28
|
from constants import ENABLE_ROBUST_PLAN_WRITES
|
|
31
29
|
|
|
30
|
+
# Import atomic_write from shared lib (canonical copy)
|
|
31
|
+
try:
|
|
32
|
+
from ...lib.base.atomic_write import atomic_write
|
|
33
|
+
except ImportError:
|
|
34
|
+
# Fallback for direct execution
|
|
35
|
+
_shared_lib = Path(__file__).resolve().parent.parent.parent / "_shared" / "lib"
|
|
36
|
+
import importlib.util
|
|
37
|
+
_spec = importlib.util.spec_from_file_location(
|
|
38
|
+
"atomic_write", str(_shared_lib / "base" / "atomic_write.py")
|
|
39
|
+
)
|
|
40
|
+
_mod = importlib.util.module_from_spec(_spec)
|
|
41
|
+
_spec.loader.exec_module(_mod)
|
|
42
|
+
atomic_write = _mod.atomic_write
|
|
43
|
+
|
|
32
44
|
# Import canonical utilities from shared lib (with Windows bug fixes)
|
|
33
45
|
try:
|
|
34
46
|
from ...lib.base.utils import (
|
|
@@ -38,6 +50,7 @@ try:
|
|
|
38
50
|
sanitize_filename,
|
|
39
51
|
sanitize_title,
|
|
40
52
|
)
|
|
53
|
+
from ...lib.base.logger import log_debug, log_info, log_warn, log_error
|
|
41
54
|
except ImportError:
|
|
42
55
|
# Fallback for direct execution
|
|
43
56
|
import sys
|
|
@@ -51,6 +64,7 @@ except ImportError:
|
|
|
51
64
|
sanitize_filename,
|
|
52
65
|
sanitize_title,
|
|
53
66
|
)
|
|
67
|
+
from base.logger import log_debug, log_info, log_warn, log_error
|
|
54
68
|
|
|
55
69
|
|
|
56
70
|
# ---------------------------
|
|
@@ -138,25 +152,40 @@ def is_plan_already_reviewed(session_id: str, plan_hash: str) -> bool:
|
|
|
138
152
|
return False
|
|
139
153
|
|
|
140
154
|
|
|
155
|
+
def was_plan_previously_denied(session_id: str, plan_hash: str) -> bool:
|
|
156
|
+
"""Check if this plan hash was previously reviewed and denied."""
|
|
157
|
+
marker_path = get_review_marker_path(session_id)
|
|
158
|
+
if not marker_path.exists():
|
|
159
|
+
return False
|
|
160
|
+
try:
|
|
161
|
+
data = json.loads(marker_path.read_text(encoding="utf-8"))
|
|
162
|
+
return data.get("plan_hash") == plan_hash and data.get("decision") == "deny"
|
|
163
|
+
except Exception:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
141
167
|
def mark_plan_reviewed(
|
|
142
168
|
session_id: str,
|
|
143
169
|
plan_hash: str,
|
|
144
170
|
hook_name: str = "cc-native",
|
|
145
171
|
iteration_state: Optional[Dict[str, Any]] = None,
|
|
172
|
+
decision: str = "allow",
|
|
146
173
|
) -> None:
|
|
147
|
-
"""Mark this plan as reviewed (stores hash in marker file).
|
|
174
|
+
"""Mark this plan as reviewed (stores hash and decision in marker file).
|
|
148
175
|
|
|
149
176
|
Args:
|
|
150
177
|
session_id: The session identifier
|
|
151
178
|
plan_hash: Hash of the plan content
|
|
152
179
|
hook_name: Name of the hook (for logging)
|
|
153
180
|
iteration_state: Optional iteration state dict with current, max, verdict info
|
|
181
|
+
decision: Review decision - "allow" or "deny"
|
|
154
182
|
"""
|
|
155
183
|
marker = get_review_marker_path(session_id)
|
|
156
184
|
try:
|
|
157
185
|
data: Dict[str, Any] = {
|
|
158
186
|
"plan_hash": plan_hash,
|
|
159
187
|
"reviewed_at": datetime.now().isoformat(),
|
|
188
|
+
"decision": decision,
|
|
160
189
|
}
|
|
161
190
|
|
|
162
191
|
# Include iteration info if provided
|
|
@@ -173,45 +202,43 @@ def mark_plan_reviewed(
|
|
|
173
202
|
|
|
174
203
|
marker.write_text(json.dumps(data), encoding="utf-8")
|
|
175
204
|
iter_info = f" (iteration {data.get('iteration', {}).get('current', '?')}/{data.get('iteration', {}).get('max', '?')})" if iteration_state else ""
|
|
176
|
-
|
|
205
|
+
log_info(hook_name, f"Created review marker: {marker} (hash: {plan_hash}){iter_info}")
|
|
177
206
|
except Exception as e:
|
|
178
|
-
|
|
207
|
+
log_warn(hook_name, f"Failed to create review marker: {e}")
|
|
179
208
|
|
|
180
209
|
|
|
181
210
|
# ---------------------------
|
|
182
|
-
# Questions
|
|
211
|
+
# Questions asked state
|
|
183
212
|
# ---------------------------
|
|
184
213
|
|
|
185
|
-
def
|
|
186
|
-
"""Get path to questions-
|
|
214
|
+
def get_questions_asked_marker_path(session_id: str) -> Path:
|
|
215
|
+
"""Get path to questions-asked marker file for this session."""
|
|
187
216
|
safe_id = re.sub(r'[^a-zA-Z0-9_-]', '_', session_id)[:32]
|
|
188
|
-
return Path(tempfile.gettempdir()) / f"cc-native-questions-
|
|
217
|
+
return Path(tempfile.gettempdir()) / f"cc-native-questions-asked-{safe_id}.json"
|
|
189
218
|
|
|
190
219
|
|
|
191
|
-
def
|
|
192
|
-
"""Check if
|
|
220
|
+
def was_questions_asked(session_id: str) -> bool:
|
|
221
|
+
"""Check if AskUserQuestion was called this session.
|
|
193
222
|
|
|
194
223
|
Returns False on any error (fail-safe: allow feature to work).
|
|
195
224
|
"""
|
|
196
225
|
try:
|
|
197
|
-
|
|
198
|
-
return marker.exists()
|
|
226
|
+
return get_questions_asked_marker_path(session_id).exists()
|
|
199
227
|
except Exception:
|
|
200
228
|
return False
|
|
201
229
|
|
|
202
230
|
|
|
203
|
-
def
|
|
204
|
-
"""Mark that
|
|
231
|
+
def mark_questions_asked(session_id: str) -> bool:
|
|
232
|
+
"""Mark that AskUserQuestion was called. Returns True on success.
|
|
205
233
|
|
|
206
234
|
Only stores timestamp, no user data. Returns False on error.
|
|
207
235
|
"""
|
|
208
236
|
try:
|
|
209
|
-
marker =
|
|
210
|
-
|
|
211
|
-
marker.write_text(json.dumps(data), encoding="utf-8")
|
|
237
|
+
marker = get_questions_asked_marker_path(session_id)
|
|
238
|
+
marker.write_text(json.dumps({"asked_at": datetime.now().isoformat()}), encoding="utf-8")
|
|
212
239
|
return True
|
|
213
240
|
except Exception as e:
|
|
214
|
-
|
|
241
|
+
log_warn("utils", f"Failed to write questions-asked marker: {e}")
|
|
215
242
|
return False
|
|
216
243
|
|
|
217
244
|
|
|
@@ -257,17 +284,17 @@ def parse_json_maybe(text: str, require_fields: Optional[List[str]] = None) -> O
|
|
|
257
284
|
if isinstance(parsed, dict):
|
|
258
285
|
obj = parsed
|
|
259
286
|
parse_method = "heuristic"
|
|
260
|
-
|
|
287
|
+
log_debug("parse", f"Used heuristic extraction (chars {start}-{end})")
|
|
261
288
|
except Exception:
|
|
262
|
-
|
|
289
|
+
log_debug("parse", f"Heuristic extraction failed for candidate at chars {start}-{end}")
|
|
263
290
|
return None
|
|
264
291
|
|
|
265
292
|
# If we parsed something, validate required fields
|
|
266
293
|
if obj and require_fields:
|
|
267
294
|
missing = [f for f in require_fields if f not in obj or not obj[f]]
|
|
268
295
|
if missing:
|
|
269
|
-
|
|
270
|
-
|
|
296
|
+
log_warn("parse", f"Parsed JSON ({parse_method}) missing/empty fields: {missing}")
|
|
297
|
+
log_debug("parse", f"Keys present: {list(obj.keys())}")
|
|
271
298
|
|
|
272
299
|
return obj
|
|
273
300
|
|
|
@@ -281,7 +308,7 @@ def coerce_to_review(obj: Optional[Dict[str, Any]], default_fix_msg: str = "Retr
|
|
|
281
308
|
'default' if it was defaulted due to missing/empty summary.
|
|
282
309
|
"""
|
|
283
310
|
if not obj:
|
|
284
|
-
|
|
311
|
+
log_warn("coerce", "No object provided to coerce_to_review")
|
|
285
312
|
return False, "error", {
|
|
286
313
|
"verdict": "fail",
|
|
287
314
|
"summary": "No structured output returned.",
|
|
@@ -293,19 +320,19 @@ def coerce_to_review(obj: Optional[Dict[str, Any]], default_fix_msg: str = "Retr
|
|
|
293
320
|
|
|
294
321
|
verdict = obj.get("verdict")
|
|
295
322
|
if verdict not in ("pass", "warn", "fail"):
|
|
296
|
-
|
|
323
|
+
log_warn("coerce", f"Invalid or missing verdict '{verdict}', defaulting to 'warn'")
|
|
297
324
|
verdict = "warn"
|
|
298
325
|
|
|
299
326
|
# Log when fields are being defaulted
|
|
300
327
|
summary_raw = str(obj.get("summary", "")).strip()
|
|
301
328
|
if not summary_raw:
|
|
302
|
-
|
|
329
|
+
log_warn("coerce", "summary missing or empty from parsed output, using default")
|
|
303
330
|
# Add diagnostic output
|
|
304
|
-
|
|
331
|
+
log_debug("coerce", f"Raw object keys: {list(obj.keys()) if obj else 'None'}")
|
|
305
332
|
if obj:
|
|
306
|
-
|
|
333
|
+
log_debug("coerce", f"verdict={obj.get('verdict')}, issues_count={len(obj.get('issues', []))}")
|
|
307
334
|
if not obj.get("issues"):
|
|
308
|
-
|
|
335
|
+
log_debug("coerce", "issues array empty or missing")
|
|
309
336
|
|
|
310
337
|
norm = {
|
|
311
338
|
"verdict": verdict,
|
|
@@ -331,6 +358,48 @@ def worst_verdict(verdicts: List[str]) -> str:
|
|
|
331
358
|
return worst
|
|
332
359
|
|
|
333
360
|
|
|
361
|
+
def compute_review_decision(
|
|
362
|
+
all_verdicts: List[str],
|
|
363
|
+
warn_threshold: float = 0.5,
|
|
364
|
+
) -> Tuple[bool, str, float]:
|
|
365
|
+
"""Verdict aggregation: only fail triggers a block.
|
|
366
|
+
|
|
367
|
+
Fail Veto: Any fail -> deny. From safety engineering (ISO 61508) —
|
|
368
|
+
critical alarms use zero-tolerance.
|
|
369
|
+
|
|
370
|
+
Warns are informational only — the warn_ratio is computed for logging
|
|
371
|
+
and visibility but does NOT trigger blocking.
|
|
372
|
+
|
|
373
|
+
Error exclusion: Detectors that produce no signal (error/skip) are excluded
|
|
374
|
+
from the denominator. They provide no information about plan quality.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
all_verdicts: List of verdict strings from all reviewers.
|
|
378
|
+
warn_threshold: Kept for backward compatibility. No longer used for blocking.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Tuple of (should_deny, reason, score).
|
|
382
|
+
- should_deny: True if the plan should be denied.
|
|
383
|
+
- reason: "fail_veto", "acceptable", or "no_signal".
|
|
384
|
+
- score: 1.0 for fail_veto, warn_ratio for informational cases, 0.0 for no_signal.
|
|
385
|
+
"""
|
|
386
|
+
# Exclude non-signal verdicts
|
|
387
|
+
signal_verdicts = [v for v in all_verdicts if v in ("pass", "warn", "fail")]
|
|
388
|
+
|
|
389
|
+
if not signal_verdicts:
|
|
390
|
+
return False, "no_signal", 0.0
|
|
391
|
+
|
|
392
|
+
# Only fail blocks — warns are informational
|
|
393
|
+
fail_count = signal_verdicts.count("fail")
|
|
394
|
+
if fail_count > 0:
|
|
395
|
+
return True, "fail_veto", 1.0
|
|
396
|
+
|
|
397
|
+
# Warn ratio still computed for logging/visibility, but does NOT block
|
|
398
|
+
warn_count = signal_verdicts.count("warn")
|
|
399
|
+
warn_ratio = warn_count / len(signal_verdicts)
|
|
400
|
+
return False, "acceptable", warn_ratio
|
|
401
|
+
|
|
402
|
+
|
|
334
403
|
# ---------------------------
|
|
335
404
|
# Artifact writing
|
|
336
405
|
# ---------------------------
|
|
@@ -541,6 +610,104 @@ def format_combined_markdown(
|
|
|
541
610
|
return "\n".join(lines).strip() + "\n"
|
|
542
611
|
|
|
543
612
|
|
|
613
|
+
def build_inline_review_summary(
|
|
614
|
+
combined: CombinedReviewResult,
|
|
615
|
+
max_issues: int = 5,
|
|
616
|
+
max_chars: int = 800,
|
|
617
|
+
) -> str:
|
|
618
|
+
"""Build compact inline summary of HIGH-severity review findings for additionalContext.
|
|
619
|
+
|
|
620
|
+
Returns an overall verdict line plus up to 5 high-severity issues as bullet points.
|
|
621
|
+
Per-reviewer verdicts, missing sections, and key questions are omitted from inline
|
|
622
|
+
output (they remain in the full review artifact on disk).
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
combined: The combined review result from all reviewers.
|
|
626
|
+
max_issues: Maximum number of high-severity issues to include.
|
|
627
|
+
max_chars: Character budget for the summary (truncated if exceeded).
|
|
628
|
+
|
|
629
|
+
Returns:
|
|
630
|
+
Compact summary string, or empty string if no high-severity findings.
|
|
631
|
+
"""
|
|
632
|
+
# Collect HIGH severity issues across all reviewers
|
|
633
|
+
all_reviewers: List[ReviewerResult] = []
|
|
634
|
+
all_reviewers.extend(combined.cli_reviewers.values())
|
|
635
|
+
all_reviewers.extend(combined.agents.values())
|
|
636
|
+
|
|
637
|
+
high_issues: List[Dict[str, Any]] = []
|
|
638
|
+
for r in all_reviewers:
|
|
639
|
+
if not r.data:
|
|
640
|
+
continue
|
|
641
|
+
for issue in r.data.get("issues", []):
|
|
642
|
+
if issue.get("severity") == "high":
|
|
643
|
+
high_issues.append({**issue, "_reviewer": r.name})
|
|
644
|
+
|
|
645
|
+
parts: List[str] = []
|
|
646
|
+
|
|
647
|
+
# Overall verdict line
|
|
648
|
+
parts.append(f"**Plan Review: {combined.overall_verdict.upper()}**"
|
|
649
|
+
+ (f" ({len(high_issues)} high-severity issue{'s' if len(high_issues) != 1 else ''})"
|
|
650
|
+
if high_issues else ""))
|
|
651
|
+
|
|
652
|
+
# High-severity issue bullets (max 5)
|
|
653
|
+
for issue in high_issues[:max_issues]:
|
|
654
|
+
cat = issue.get("category", "general")
|
|
655
|
+
text = issue.get("issue", "")
|
|
656
|
+
fix = issue.get("suggested_fix", "")
|
|
657
|
+
reviewer = issue.get("_reviewer", "unknown")
|
|
658
|
+
line = f"- [{cat}] {text}"
|
|
659
|
+
if fix:
|
|
660
|
+
line += f" \u2192 {fix}"
|
|
661
|
+
line += f" ({reviewer})"
|
|
662
|
+
parts.append(line)
|
|
663
|
+
remaining = len(high_issues) - max_issues
|
|
664
|
+
if remaining > 0:
|
|
665
|
+
parts.append(f" ...and {remaining} more")
|
|
666
|
+
|
|
667
|
+
result = "\n".join(parts)
|
|
668
|
+
if len(result) > max_chars:
|
|
669
|
+
result = result[:max_chars - 3] + "..."
|
|
670
|
+
return result
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def extract_top_issues_text(
|
|
674
|
+
combined: CombinedReviewResult,
|
|
675
|
+
max_count: int = 3,
|
|
676
|
+
severity: str = "high",
|
|
677
|
+
) -> str:
|
|
678
|
+
"""Extract top issues as a compact text string for permissionDecisionReason.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
combined: The combined review result.
|
|
682
|
+
max_count: Maximum number of issues to include.
|
|
683
|
+
severity: Severity level to filter for.
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
Compact semicolon-separated issue text.
|
|
687
|
+
"""
|
|
688
|
+
all_reviewers: List[ReviewerResult] = []
|
|
689
|
+
all_reviewers.extend(combined.cli_reviewers.values())
|
|
690
|
+
all_reviewers.extend(combined.agents.values())
|
|
691
|
+
|
|
692
|
+
issues: List[str] = []
|
|
693
|
+
for r in all_reviewers:
|
|
694
|
+
if not r.data:
|
|
695
|
+
continue
|
|
696
|
+
for issue in r.data.get("issues", []):
|
|
697
|
+
if issue.get("severity") == severity:
|
|
698
|
+
text = issue.get("issue", "").strip()
|
|
699
|
+
if text:
|
|
700
|
+
issues.append(text)
|
|
701
|
+
if len(issues) >= max_count:
|
|
702
|
+
break
|
|
703
|
+
if len(issues) >= max_count:
|
|
704
|
+
break
|
|
705
|
+
|
|
706
|
+
if not issues:
|
|
707
|
+
return "Review found critical issues"
|
|
708
|
+
return "; ".join(issues)
|
|
709
|
+
|
|
710
|
+
|
|
544
711
|
def _append_review_details(
|
|
545
712
|
lines: List[str],
|
|
546
713
|
data: Dict[str, Any],
|
|
@@ -748,13 +915,13 @@ def write_combined_artifacts(
|
|
|
748
915
|
if not out_dir:
|
|
749
916
|
raise ValueError("Either context_reviews_dir or review_folder is required")
|
|
750
917
|
|
|
751
|
-
|
|
918
|
+
log_debug("utils", f"Using review folder: {out_dir}")
|
|
752
919
|
|
|
753
920
|
# Check directory creation explicitly
|
|
754
921
|
try:
|
|
755
922
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
756
923
|
except PermissionError as e:
|
|
757
|
-
|
|
924
|
+
log_error("utils", f"Cannot create directory {out_dir}: {e}")
|
|
758
925
|
raise
|
|
759
926
|
|
|
760
927
|
# JSON write with atomic operation - use combined.json for folder-based
|
|
@@ -769,7 +936,7 @@ def write_combined_artifacts(
|
|
|
769
936
|
else:
|
|
770
937
|
json_path.write_text(json.dumps(json_data, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
771
938
|
except Exception as e:
|
|
772
|
-
|
|
939
|
+
log_error("utils", f"Failed to write {json_path.name}: {e}")
|
|
773
940
|
raise
|
|
774
941
|
|
|
775
942
|
# Markdown write with atomic operation - use combined.md for folder-based
|
|
@@ -784,7 +951,7 @@ def write_combined_artifacts(
|
|
|
784
951
|
else:
|
|
785
952
|
md_path.write_text(md_content, encoding="utf-8")
|
|
786
953
|
except Exception as e:
|
|
787
|
-
|
|
954
|
+
log_error("utils", f"Failed to write {md_path.name}: {e}")
|
|
788
955
|
raise
|
|
789
956
|
|
|
790
957
|
# Individual reviewer writes (non-critical - continue on failure)
|
|
@@ -796,11 +963,11 @@ def write_combined_artifacts(
|
|
|
796
963
|
if ENABLE_ROBUST_PLAN_WRITES:
|
|
797
964
|
success, error = atomic_write(reviewer_path, content)
|
|
798
965
|
if not success:
|
|
799
|
-
|
|
966
|
+
log_warn("utils", f"Failed to write {reviewer_path.name}: {error}")
|
|
800
967
|
else:
|
|
801
968
|
reviewer_path.write_text(content, encoding="utf-8")
|
|
802
969
|
except Exception as e:
|
|
803
|
-
|
|
970
|
+
log_warn("utils", f"Failed to write {reviewer_path.name}: {e}")
|
|
804
971
|
# Continue - individual reviewer failures not critical
|
|
805
972
|
for name, r in result.agents.items():
|
|
806
973
|
if r.data:
|
|
@@ -810,11 +977,11 @@ def write_combined_artifacts(
|
|
|
810
977
|
if ENABLE_ROBUST_PLAN_WRITES:
|
|
811
978
|
success, error = atomic_write(reviewer_path, content)
|
|
812
979
|
if not success:
|
|
813
|
-
|
|
980
|
+
log_warn("utils", f"Failed to write {reviewer_path.name}: {error}")
|
|
814
981
|
else:
|
|
815
982
|
reviewer_path.write_text(content, encoding="utf-8")
|
|
816
983
|
except Exception as e:
|
|
817
|
-
|
|
984
|
+
log_warn("utils", f"Failed to write {reviewer_path.name}: {e}")
|
|
818
985
|
# Continue - individual reviewer failures not critical
|
|
819
986
|
|
|
820
987
|
# Generate index.md for folder-based reviews
|
|
@@ -825,11 +992,11 @@ def write_combined_artifacts(
|
|
|
825
992
|
if ENABLE_ROBUST_PLAN_WRITES:
|
|
826
993
|
success, error = atomic_write(index_path, index_content)
|
|
827
994
|
if not success:
|
|
828
|
-
|
|
995
|
+
log_warn("utils", f"Failed to write index.md: {error}")
|
|
829
996
|
else:
|
|
830
997
|
index_path.write_text(index_content, encoding="utf-8")
|
|
831
998
|
except Exception as e:
|
|
832
|
-
|
|
999
|
+
log_warn("utils", f"Failed to write index.md: {e}")
|
|
833
1000
|
|
|
834
1001
|
return index_path
|
|
835
1002
|
|
|
@@ -849,7 +1016,7 @@ def load_config(project_dir: Path) -> Dict[str, Any]:
|
|
|
849
1016
|
with open(settings_path, "r", encoding="utf-8") as f:
|
|
850
1017
|
return json.load(f)
|
|
851
1018
|
except Exception as e:
|
|
852
|
-
|
|
1019
|
+
log_warn("cc-native", f"Failed to load config: {e}")
|
|
853
1020
|
return {}
|
|
854
1021
|
|
|
855
1022
|
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"timeout": 120
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
|
-
"blockOnFail": false,
|
|
17
16
|
"display": {
|
|
18
17
|
"maxIssues": 12,
|
|
19
18
|
"maxMissingSections": 12,
|
|
@@ -23,7 +22,7 @@
|
|
|
23
22
|
"agentReview": {
|
|
24
23
|
"enabled": true,
|
|
25
24
|
"timeout": 180,
|
|
26
|
-
"
|
|
25
|
+
"warnThreshold": 0.01,
|
|
27
26
|
"orchestrator": {
|
|
28
27
|
"enabled": true,
|
|
29
28
|
"model": "haiku",
|
package/oclif.manifest.json
CHANGED