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.
Files changed (116) hide show
  1. package/bin/run.js +5 -2
  2. package/dist/lib/claude-settings-types.d.ts +2 -0
  3. package/dist/templates/CLAUDE.md +3 -3
  4. package/dist/templates/_shared/.claude/settings.json +4 -0
  5. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  7. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  8. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  9. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  10. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  11. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  12. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  13. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  15. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/hooks/archive_plan.py +87 -178
  17. package/dist/templates/_shared/hooks/context_monitor.py +104 -247
  18. package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
  19. package/dist/templates/_shared/hooks/pre_compact.py +47 -32
  20. package/dist/templates/_shared/hooks/session_end.py +103 -60
  21. package/dist/templates/_shared/hooks/session_start.py +110 -81
  22. package/dist/templates/_shared/hooks/task_create_capture.py +26 -50
  23. package/dist/templates/_shared/hooks/task_update_capture.py +42 -115
  24. package/dist/templates/_shared/hooks/user_prompt_submit.py +61 -61
  25. package/dist/templates/_shared/lib/base/__init__.py +16 -0
  26. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  27. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  28. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  29. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  30. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  31. package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
  32. package/dist/templates/_shared/lib/base/inference.py +121 -0
  33. package/dist/templates/_shared/lib/base/logger.py +291 -0
  34. package/dist/templates/_shared/lib/base/utils.py +42 -9
  35. package/dist/templates/_shared/lib/context/__init__.py +72 -80
  36. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  37. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  38. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  39. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  40. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  41. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  42. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  43. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  44. package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
  45. package/dist/templates/_shared/lib/context/context_selector.py +491 -0
  46. package/dist/templates/_shared/lib/context/context_store.py +636 -0
  47. package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
  48. package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
  49. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  50. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  51. package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
  52. package/dist/templates/_shared/lib/templates/README.md +5 -13
  53. package/dist/templates/_shared/lib/templates/__init__.py +2 -6
  54. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  55. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  56. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  57. package/dist/templates/_shared/lib/templates/plan_context.py +1 -38
  58. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  59. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  60. package/dist/templates/_shared/scripts/save_handoff.py +39 -19
  61. package/dist/templates/_shared/scripts/status_line.py +701 -0
  62. package/dist/templates/_shared/workflows/handoff.md +9 -3
  63. package/dist/templates/cc-native/.claude/settings.json +41 -8
  64. package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
  65. package/dist/templates/cc-native/MIGRATION.md +1 -1
  66. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
  67. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +49 -21
  68. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  69. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  70. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  71. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  72. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  73. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  74. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -55
  75. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +163 -131
  76. package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
  78. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
  79. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +6 -4
  80. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  81. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  82. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  83. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  84. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  85. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
  87. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +34 -29
  88. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  89. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  90. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  91. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  92. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  93. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
  94. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
  95. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
  96. package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
  97. package/dist/templates/cc-native/_cc-native/lib/utils.py +207 -40
  98. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
  99. package/oclif.manifest.json +1 -1
  100. package/package.json +1 -1
  101. package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
  102. package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -177
  103. package/dist/templates/_shared/lib/context/auto_state.py +0 -167
  104. package/dist/templates/_shared/lib/context/cache.py +0 -444
  105. package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
  106. package/dist/templates/_shared/lib/context/context_manager.py +0 -1057
  107. package/dist/templates/_shared/lib/context/discovery.py +0 -554
  108. package/dist/templates/_shared/lib/context/event_log.py +0 -316
  109. package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
  110. package/dist/templates/_shared/lib/context/task_sync.py +0 -407
  111. package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
  112. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  113. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  114. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  115. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
  116. 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
- eprint(f"[{hook_name}] Created review marker: {marker} (hash: {plan_hash}){iter_info}")
205
+ log_info(hook_name, f"Created review marker: {marker} (hash: {plan_hash}){iter_info}")
177
206
  except Exception as e:
178
- eprint(f"[{hook_name}] Warning: failed to create review marker: {e}")
207
+ log_warn(hook_name, f"Failed to create review marker: {e}")
179
208
 
180
209
 
181
210
  # ---------------------------
182
- # Questions offered state
211
+ # Questions asked state
183
212
  # ---------------------------
184
213
 
185
- def get_questions_marker_path(session_id: str) -> Path:
186
- """Get path to questions-offered marker file for this session."""
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-offered-{safe_id}.json"
217
+ return Path(tempfile.gettempdir()) / f"cc-native-questions-asked-{safe_id}.json"
189
218
 
190
219
 
191
- def was_questions_offered(session_id: str) -> bool:
192
- """Check if clarifying questions were already offered this session.
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
- marker = get_questions_marker_path(session_id)
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 mark_questions_offered(session_id: str) -> bool:
204
- """Mark that questions were offered. Returns True on success.
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 = get_questions_marker_path(session_id)
210
- data = {"offered_at": datetime.now().isoformat()}
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
- eprint(f"[utils] Failed to write questions marker: {e}")
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
- eprint(f"[parse] Used heuristic extraction (chars {start}-{end})")
287
+ log_debug("parse", f"Used heuristic extraction (chars {start}-{end})")
261
288
  except Exception:
262
- eprint(f"[parse] Heuristic extraction failed for candidate at chars {start}-{end}")
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
- eprint(f"[parse] WARNING: parsed JSON ({parse_method}) missing/empty fields: {missing}")
270
- eprint(f"[parse] Keys present: {list(obj.keys())}")
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
- eprint("[coerce] WARNING: No object provided to coerce_to_review")
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
- eprint(f"[coerce] WARNING: Invalid or missing verdict '{verdict}', defaulting to 'warn'")
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
- eprint("[coerce] WARNING: summary missing or empty from parsed output, using default")
329
+ log_warn("coerce", "summary missing or empty from parsed output, using default")
303
330
  # Add diagnostic output
304
- eprint(f"[coerce] Raw object keys: {list(obj.keys()) if obj else 'None'}")
331
+ log_debug("coerce", f"Raw object keys: {list(obj.keys()) if obj else 'None'}")
305
332
  if obj:
306
- eprint(f"[coerce] verdict={obj.get('verdict')}, issues_count={len(obj.get('issues', []))}")
333
+ log_debug("coerce", f"verdict={obj.get('verdict')}, issues_count={len(obj.get('issues', []))}")
307
334
  if not obj.get("issues"):
308
- eprint("[coerce] INFO: issues array empty or missing")
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
- eprint(f"[utils] Using review folder: {out_dir}")
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
- eprint(f"[utils] FATAL: Cannot create directory {out_dir}: {e}")
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
- eprint(f"[utils] FATAL: Failed to write {json_path.name}: {e}")
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
- eprint(f"[utils] FATAL: Failed to write {md_path.name}: {e}")
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
- eprint(f"[utils] WARNING: Failed to write {reviewer_path.name}: {error}")
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
- eprint(f"[utils] WARNING: Failed to write {reviewer_path.name}: {e}")
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
- eprint(f"[utils] WARNING: Failed to write {reviewer_path.name}: {error}")
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
- eprint(f"[utils] WARNING: Failed to write {reviewer_path.name}: {e}")
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
- eprint(f"[utils] WARNING: Failed to write index.md: {error}")
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
- eprint(f"[utils] WARNING: Failed to write index.md: {e}")
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
- eprint(f"[cc-native] Failed to load config: {e}")
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
- "blockOnFail": true,
25
+ "warnThreshold": 0.01,
27
26
  "orchestrator": {
28
27
  "enabled": true,
29
28
  "model": "haiku",
@@ -401,5 +401,5 @@
401
401
  ]
402
402
  }
403
403
  },
404
- "version": "0.9.8"
404
+ "version": "0.10.0"
405
405
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aiwcli",
3
3
  "description": "AI Workflow CLI - Command-line interface for AI-powered workflows",
4
- "version": "0.9.8",
4
+ "version": "0.10.0",
5
5
  "author": "jofu-tofu",
6
6
  "bin": {
7
7
  "aiw": "./bin/run.js"