aiwcli 0.9.1 → 0.9.4

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 (90) hide show
  1. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  2. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  3. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  4. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  5. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  6. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  7. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  8. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  9. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  10. package/dist/templates/_shared/hooks/archive_plan.py +28 -38
  11. package/dist/templates/_shared/hooks/context_enforcer.py +71 -21
  12. package/dist/templates/_shared/hooks/context_monitor.py +4 -8
  13. package/dist/templates/_shared/hooks/file-suggestion.py +4 -10
  14. package/dist/templates/_shared/hooks/session_start.py +103 -0
  15. package/dist/templates/_shared/hooks/task_create_atomicity.py +205 -0
  16. package/dist/templates/_shared/hooks/task_create_capture.py +83 -146
  17. package/dist/templates/_shared/hooks/task_update_capture.py +116 -167
  18. package/dist/templates/_shared/hooks/user_prompt_submit.py +62 -22
  19. package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  20. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  21. package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
  22. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  23. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  24. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  25. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  26. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  27. package/dist/templates/_shared/lib/base/hook_utils.py +169 -0
  28. package/dist/templates/_shared/lib/base/inference.py +20 -35
  29. package/dist/templates/_shared/lib/base/stop_words.py +158 -0
  30. package/dist/templates/_shared/lib/base/utils.py +3 -2
  31. package/dist/templates/_shared/lib/context/__init__.py +9 -2
  32. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  33. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  34. package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
  35. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  36. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  37. package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
  38. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  39. package/dist/templates/_shared/lib/context/context_extractor.py +115 -0
  40. package/dist/templates/_shared/lib/context/context_manager.py +2 -2
  41. package/dist/templates/_shared/lib/context/discovery.py +4 -4
  42. package/dist/templates/_shared/lib/context/task_sync.py +5 -82
  43. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  44. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  45. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  46. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  47. package/dist/templates/_shared/lib/templates/persona_questions.py +113 -0
  48. package/dist/templates/_shared/lib/templates/plan_context.py +13 -27
  49. package/dist/templates/cc-native/.claude/agents/cc-native/ACCESSIBILITY-TESTER.md +1 -1
  50. package/dist/templates/cc-native/.claude/agents/cc-native/ARCHITECT-REVIEWER.md +21 -48
  51. package/dist/templates/cc-native/.claude/agents/cc-native/ASSUMPTION-CHAIN-TRACER.md +26 -204
  52. package/dist/templates/cc-native/.claude/agents/cc-native/CLARITY-AUDITOR.md +25 -76
  53. package/dist/templates/cc-native/.claude/agents/cc-native/CODE-REVIEWER.md +1 -1
  54. package/dist/templates/cc-native/.claude/agents/cc-native/COMPLETENESS-CHECKER.md +32 -77
  55. package/dist/templates/cc-native/.claude/agents/cc-native/CONTEXT-EXTRACTOR.md +1 -1
  56. package/dist/templates/cc-native/.claude/agents/cc-native/DEVILS-ADVOCATE.md +26 -189
  57. package/dist/templates/cc-native/.claude/agents/cc-native/DOCUMENTATION-REVIEWER.md +30 -52
  58. package/dist/templates/cc-native/.claude/agents/cc-native/FEASIBILITY-ANALYST.md +27 -63
  59. package/dist/templates/cc-native/.claude/agents/cc-native/FRESH-PERSPECTIVE.md +32 -81
  60. package/dist/templates/cc-native/.claude/agents/cc-native/HANDOFF-READINESS.md +25 -106
  61. package/dist/templates/cc-native/.claude/agents/cc-native/HIDDEN-COMPLEXITY-DETECTOR.md +24 -209
  62. package/dist/templates/cc-native/.claude/agents/cc-native/INCENTIVE-MAPPER.md +26 -200
  63. package/dist/templates/cc-native/.claude/agents/cc-native/PENETRATION-TESTER.md +1 -1
  64. package/dist/templates/cc-native/.claude/agents/cc-native/PERFORMANCE-ENGINEER.md +1 -1
  65. package/dist/templates/cc-native/.claude/agents/cc-native/PLAN-ORCHESTRATOR.md +1 -1
  66. package/dist/templates/cc-native/.claude/agents/cc-native/PRECEDENT-FINDER.md +36 -206
  67. package/dist/templates/cc-native/.claude/agents/cc-native/REVERSIBILITY-ANALYST.md +27 -177
  68. package/dist/templates/cc-native/.claude/agents/cc-native/RISK-ASSESSOR.md +23 -66
  69. package/dist/templates/cc-native/.claude/agents/cc-native/SECOND-ORDER-ANALYST.md +26 -162
  70. package/dist/templates/cc-native/.claude/agents/cc-native/SIMPLICITY-GUARDIAN.md +29 -59
  71. package/dist/templates/cc-native/.claude/agents/cc-native/SKEPTIC.md +28 -312
  72. package/dist/templates/cc-native/.claude/agents/cc-native/STAKEHOLDER-ADVOCATE.md +23 -74
  73. package/dist/templates/cc-native/.claude/agents/cc-native/TRADE-OFF-ILLUMINATOR.md +1 -1
  74. package/dist/templates/cc-native/.claude/settings.json +21 -0
  75. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +211 -0
  76. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  78. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  79. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +65 -12
  80. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +240 -0
  81. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  82. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  83. package/dist/templates/cc-native/_cc-native/lib/debug.py +124 -0
  84. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +1 -0
  85. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +34 -1
  87. package/dist/templates/cc-native/_cc-native/plan-review.config.json +7 -1
  88. package/oclif.manifest.json +1 -1
  89. package/package.json +1 -1
  90. package/dist/templates/cc-native/_cc-native/docs/PERMISSION_REQUEST_VERIFICATION.md +0 -147
@@ -27,47 +27,31 @@ Usage in .claude/settings.json:
27
27
  }
28
28
  """
29
29
  import json
30
+ import re
30
31
  import sys
31
32
  from pathlib import Path
32
33
  from typing import Optional
33
34
 
34
35
  # Add parent directories to path for imports
35
- script_dir = Path(__file__).resolve().parent
36
- lib_dir = script_dir.parent / "lib"
37
- sys.path.insert(0, str(lib_dir.parent))
36
+ SCRIPT_DIR = Path(__file__).resolve().parent
37
+ SHARED_LIB = SCRIPT_DIR.parent / "lib"
38
+ sys.path.insert(0, str(SHARED_LIB.parent))
38
39
 
40
+ from lib.base.hook_utils import load_hook_input
39
41
  from lib.context.plan_archive import archive_plan_to_context
40
42
  from lib.context.context_manager import get_all_contexts
43
+ from lib.context.context_extractor import extract_context_id_for_session
41
44
  from lib.base.utils import eprint, project_dir
45
+ from lib.base.constants import get_context_dir
42
46
 
43
-
44
- def get_context_for_session(session_id: str, project_root: Path) -> Optional[str]:
45
- """
46
- Find context that matches this session_id.
47
-
48
- Args:
49
- session_id: Session ID to match
50
- project_root: Project root directory
51
-
52
- Returns:
53
- Context ID or None if not found
54
- """
55
- contexts = get_all_contexts(status="active", project_root=project_root)
56
-
57
- # Primary strategy: Find context with matching session_id
58
- for ctx in contexts:
59
- if ctx.in_flight and ctx.in_flight.session_ids and session_id in ctx.in_flight.session_ids:
60
- eprint(f"[archive_plan] Found context by session: {ctx.id}")
61
- return ctx.id
62
-
63
- # Fallback: If only one context is planning, assume it's the one
64
- planning_contexts = [c for c in contexts if c.in_flight and c.in_flight.mode == "planning"]
65
- if len(planning_contexts) == 1:
66
- eprint(f"[archive_plan] Fallback: Single planning context: {planning_contexts[0].id}")
67
- return planning_contexts[0].id
68
-
69
- eprint(f"[archive_plan] Could not find context for session {session_id}")
70
- return None
47
+ # Import debug cleanup function from cc-native lib
48
+ _cc_native_lib = SCRIPT_DIR.parent / "_cc-native" / "lib"
49
+ sys.path.insert(0, str(_cc_native_lib))
50
+ try:
51
+ from debug import cleanup_debug_folder
52
+ except ImportError:
53
+ def cleanup_debug_folder(context_path):
54
+ pass # Fallback if debug module not available
71
55
 
72
56
 
73
57
  def extract_plan_path_from_result(tool_result: str) -> Optional[str]:
@@ -76,7 +60,6 @@ def extract_plan_path_from_result(tool_result: str) -> Optional[str]:
76
60
 
77
61
  Looks for pattern: "Your plan has been saved to: <path>"
78
62
  """
79
- import re
80
63
  match = re.search(r'Your plan has been saved to:\s*(.+\.md)', tool_result)
81
64
  if match:
82
65
  return match.group(1).strip()
@@ -90,10 +73,9 @@ def on_plan_archive():
90
73
  Called from PostToolUse on ExitPlanMode - extracts plan path from result
91
74
  and archives to the active context.
92
75
  """
93
- # Read hook input from stdin
94
- try:
95
- hook_input = json.load(sys.stdin)
96
- except json.JSONDecodeError:
76
+ # Read hook input using shared utility
77
+ hook_input = load_hook_input()
78
+ if not hook_input:
97
79
  eprint("[archive_plan] No valid JSON input")
98
80
  return
99
81
 
@@ -220,9 +202,9 @@ def on_plan_archive():
220
202
  print(f"Plan archival skipped: file not found ({plan_path})")
221
203
  return
222
204
 
223
- # Find context by session ID
205
+ # Find context by session ID using shared extractor
224
206
  session_id = hook_input.get("session_id", "unknown")
225
- context_id = get_context_for_session(session_id, project_root)
207
+ context_id = extract_context_id_for_session(session_id, project_root, "archive_plan")
226
208
 
227
209
  if not context_id:
228
210
  eprint("[archive_plan] Could not determine context for session")
@@ -246,6 +228,14 @@ def on_plan_archive():
246
228
  )
247
229
 
248
230
  if archived_path:
231
+ # Clean up debug logs before completing archive
232
+ try:
233
+ context_path = get_context_dir(context_id, project_root)
234
+ cleanup_debug_folder(context_path)
235
+ print(f"[archive_plan] Cleaned up debug logs for context: {context_id}")
236
+ except Exception as e:
237
+ print(f"[archive_plan] Warning: could not clean debug folder: {e}")
238
+
249
239
  print(f"")
250
240
  print(f"[archive_plan] SUCCESS!")
251
241
  print(f"[archive_plan] Plan archived to context: {context_id}")
@@ -49,6 +49,7 @@ SCRIPT_DIR = Path(__file__).resolve().parent
49
49
  SHARED_LIB = SCRIPT_DIR.parent / "lib"
50
50
  sys.path.insert(0, str(SHARED_LIB.parent))
51
51
 
52
+ from lib.base.hook_utils import load_hook_input
52
53
  from lib.base.subprocess_utils import is_internal_call
53
54
  from lib.context.context_manager import (
54
55
  Context,
@@ -65,7 +66,7 @@ from lib.context.discovery import (
65
66
  format_context_created,
66
67
  format_pending_plan_continuation,
67
68
  format_implementation_continuation,
68
- _format_relative_time,
69
+ format_relative_time,
69
70
  )
70
71
  from lib.templates.formatters import get_mode_display
71
72
  from lib.base.utils import eprint, project_dir
@@ -251,7 +252,7 @@ def format_context_picker_stderr(contexts: List[Context]) -> str:
251
252
 
252
253
  implementing_count = 0
253
254
  for i, ctx in enumerate(contexts, 1):
254
- time_str = _format_relative_time(ctx.last_active)
255
+ time_str = format_relative_time(ctx.last_active)
255
256
 
256
257
  # Check if context is in implementing mode (selectable)
257
258
  is_implementing = ctx.in_flight and ctx.in_flight.mode == "implementing"
@@ -299,13 +300,18 @@ def format_context_picker_stderr(contexts: List[Context]) -> str:
299
300
  return "\n".join(lines)
300
301
 
301
302
 
302
- def format_command_feedback(ended_contexts: List[Context], selected_context: Optional[Context]) -> str:
303
+ def format_command_feedback(
304
+ ended_contexts: List[Context],
305
+ selected_context: Optional[Context],
306
+ remaining_prompt: Optional[str] = None
307
+ ) -> str:
303
308
  """
304
309
  Format feedback about what context operations were performed.
305
310
 
306
311
  Args:
307
312
  ended_contexts: Contexts that were ended/completed
308
313
  selected_context: Context that was selected (if any)
314
+ remaining_prompt: User's actual request after caret command (if any)
309
315
 
310
316
  Returns:
311
317
  Formatted feedback message
@@ -331,13 +337,21 @@ def format_command_feedback(ended_contexts: List[Context], selected_context: Opt
331
337
  if mode_str:
332
338
  mode_display = mode_str.strip("[]")
333
339
 
334
- time_str = _format_relative_time(selected_context.last_active)
340
+ time_str = format_relative_time(selected_context.last_active)
335
341
  lines.append(f"**Mode:** {mode_display}")
336
342
  lines.append(f"**Last Active:** {time_str}")
337
343
  lines.append("")
338
344
  lines.append(f'All work belongs to context "{selected_context.id}".')
339
345
  lines.append("Tasks created with TaskCreate will be persisted to this context.")
340
346
 
347
+ # Add user's actual request if provided after caret command
348
+ if remaining_prompt and remaining_prompt.strip():
349
+ lines.append("")
350
+ lines.append("---")
351
+ lines.append("")
352
+ lines.append("**User's actual request:**")
353
+ lines.append(f"> {remaining_prompt}")
354
+
341
355
  return "\n".join(lines)
342
356
 
343
357
 
@@ -345,7 +359,7 @@ def determine_context(
345
359
  user_prompt: str,
346
360
  project_root: Path = None,
347
361
  session_id: str = None
348
- ) -> Tuple[Optional[str], str, Optional[str]]:
362
+ ) -> Tuple[Optional[str], str, Optional[str], Optional[str]]:
349
363
  """
350
364
  Determine which context this prompt belongs to.
351
365
 
@@ -355,6 +369,7 @@ def determine_context(
355
369
  - method: How context was determined (session_match, in_flight, caret_select,
356
370
  auto_created, single_context, blocked)
357
371
  - output: System reminder to inject, or None
372
+ - remaining_prompt: Actual user request after caret command, or None
358
373
 
359
374
  Raises:
360
375
  BlockRequest: When request should be blocked to show picker to user
@@ -362,7 +377,7 @@ def determine_context(
362
377
  # 0. Skip context creation for internal subprocess calls (orchestrator, agents)
363
378
  if is_internal_call():
364
379
  eprint("[context_enforcer] Skipping: internal subprocess call")
365
- return (None, "skip_internal", None)
380
+ return (None, "skip_internal", None, None)
366
381
 
367
382
  # 1. Check if session already belongs to a context (HIGHEST PRIORITY)
368
383
  # This prevents context switching on subsequent prompts - one context per session
@@ -373,11 +388,20 @@ def determine_context(
373
388
  return (
374
389
  session_context.id,
375
390
  "session_match",
376
- format_active_context_reminder(session_context)
391
+ format_active_context_reminder(session_context),
392
+ None
377
393
  )
378
394
 
379
395
  # 2. Check for bare "^" - show context picker
380
396
  if user_prompt.strip() == "^":
397
+ # Pre-transition: Move any pending_implementation contexts to implementing
398
+ # This ensures they appear selectable when user opens the picker
399
+ in_flight = get_all_in_flight_contexts(project_root)
400
+ for ctx in in_flight:
401
+ if ctx.in_flight and ctx.in_flight.mode == "pending_implementation":
402
+ update_plan_status(ctx.id, "implementing", project_root=project_root)
403
+ eprint(f"[context_enforcer] Pre-transitioned {ctx.id} to implementing (bare caret)")
404
+
381
405
  contexts = get_all_contexts(status="active", project_root=project_root)
382
406
  if not contexts:
383
407
  raise BlockRequest(
@@ -407,7 +431,7 @@ def determine_context(
407
431
 
408
432
  # Don't auto-create for greetings or help commands
409
433
  if any(prompt_lower.startswith(p) or prompt_lower == p for p in skip_patterns):
410
- return (None, "no_context_needed", None)
434
+ return (None, "no_context_needed", None, None)
411
435
 
412
436
  # Auto-create context from prompt
413
437
  try:
@@ -419,11 +443,12 @@ def determine_context(
419
443
  return (
420
444
  new_context.id,
421
445
  "auto_created",
422
- format_context_created(new_context)
446
+ format_context_created(new_context),
447
+ None
423
448
  )
424
449
  except Exception as e:
425
450
  eprint(f"[context_enforcer] Failed to create context: {e}")
426
- return (None, "creation_failed", None)
451
+ return (None, "creation_failed", None, None)
427
452
 
428
453
  elif len(in_flight_contexts) == 1:
429
454
  # Single in-flight context - auto-select it
@@ -431,6 +456,15 @@ def determine_context(
431
456
  mode = ctx.in_flight.mode if ctx.in_flight else "none"
432
457
  eprint(f"[context_enforcer] Auto-selected single in-flight context: {ctx.id} (mode={mode})")
433
458
 
459
+ # Auto-transition pending_implementation to implementing
460
+ # This ensures state updates immediately when context is selected,
461
+ # rather than waiting for _update_in_flight_status() which has conditions
462
+ if mode == "pending_implementation":
463
+ update_plan_status(ctx.id, "implementing", project_root=project_root)
464
+ ctx.in_flight.mode = "implementing" # Update local copy for display
465
+ mode = "implementing" # Update local var for formatter selection
466
+ eprint(f"[context_enforcer] Transitioned {ctx.id} to implementing")
467
+
434
468
  # Use mode-specific formatter for better continuation context
435
469
  if mode == "pending_implementation":
436
470
  output = format_pending_plan_continuation(ctx)
@@ -439,7 +473,7 @@ def determine_context(
439
473
  else:
440
474
  output = format_active_context_reminder(ctx)
441
475
 
442
- return (ctx.id, "auto_selected", output)
476
+ return (ctx.id, "auto_selected", output, None)
443
477
 
444
478
  else:
445
479
  # Multiple in-flight contexts - block and show picker
@@ -455,7 +489,7 @@ def _handle_caret_command(
455
489
  user_prompt: str,
456
490
  contexts: List[Context],
457
491
  project_root: Path
458
- ) -> Tuple[Optional[str], str, Optional[str]]:
492
+ ) -> Tuple[Optional[str], str, Optional[str], Optional[str]]:
459
493
  """
460
494
  Handle explicit caret commands (^E, ^S, ^0, ^N).
461
495
 
@@ -465,7 +499,7 @@ def _handle_caret_command(
465
499
  project_root: Project root directory
466
500
 
467
501
  Returns:
468
- Tuple of (context_id, method, output)
502
+ Tuple of (context_id, method, output, remaining_prompt)
469
503
 
470
504
  Raises:
471
505
  BlockRequest: When command is invalid or selection needed
@@ -505,7 +539,8 @@ def _handle_caret_command(
505
539
  return (
506
540
  new_context.id,
507
541
  "caret_new",
508
- format_context_created(new_context)
542
+ format_context_created(new_context),
543
+ None
509
544
  )
510
545
  except Exception as e:
511
546
  eprint(f"[context_enforcer] Failed to create context: {e}")
@@ -542,7 +577,8 @@ def _handle_caret_command(
542
577
  return (
543
578
  new_context.id,
544
579
  "caret_new",
545
- output
580
+ output,
581
+ None
546
582
  )
547
583
  except Exception as e:
548
584
  eprint(f"[context_enforcer] Failed to create context: {e}")
@@ -552,11 +588,24 @@ def _handle_caret_command(
552
588
  if cmd.select:
553
589
  selected_ctx = contexts[cmd.select - 1] # 1-indexed
554
590
  eprint(f"[context_enforcer] Caret-selected context: {selected_ctx.id}")
555
- output = format_command_feedback(ended_contexts, selected_ctx)
591
+
592
+ # Auto-transition pending_implementation to implementing
593
+ mode = selected_ctx.in_flight.mode if selected_ctx.in_flight else "none"
594
+ if mode == "pending_implementation":
595
+ update_plan_status(selected_ctx.id, "implementing", project_root=project_root)
596
+ selected_ctx.in_flight.mode = "implementing"
597
+ eprint(f"[context_enforcer] Transitioned {selected_ctx.id} to implementing")
598
+
599
+ output = format_command_feedback(
600
+ ended_contexts,
601
+ selected_ctx,
602
+ cmd.remaining_prompt if cmd.remaining_prompt else None
603
+ )
556
604
  return (
557
605
  selected_ctx.id,
558
606
  "caret_select",
559
- output
607
+ output,
608
+ cmd.remaining_prompt if cmd.remaining_prompt else None
560
609
  )
561
610
 
562
611
  # Only ended contexts, no selection - refresh context list and block
@@ -588,11 +637,10 @@ def main():
588
637
  In production, use user_prompt_submit.py as the unified entry point.
589
638
  """
590
639
  try:
591
- input_data = sys.stdin.read().strip()
592
- if not input_data:
640
+ hook_input = load_hook_input()
641
+ if not hook_input:
593
642
  return
594
643
 
595
- hook_input = json.loads(input_data)
596
644
  user_prompt = hook_input.get("prompt", "")
597
645
  if not user_prompt:
598
646
  return
@@ -600,8 +648,10 @@ def main():
600
648
  project_root = project_dir(hook_input)
601
649
 
602
650
  try:
603
- context_id, method, output = determine_context(user_prompt, project_root)
651
+ context_id, method, output, remaining_prompt = determine_context(user_prompt, project_root)
604
652
  eprint(f"[context_enforcer] Method: {method}, Context: {context_id}")
653
+ if remaining_prompt:
654
+ eprint(f"[context_enforcer] Remaining prompt: {remaining_prompt[:50]}...")
605
655
 
606
656
  if output:
607
657
  print(output)
@@ -55,6 +55,7 @@ SCRIPT_DIR = Path(__file__).resolve().parent
55
55
  SHARED_LIB = SCRIPT_DIR.parent / "lib"
56
56
  sys.path.insert(0, str(SHARED_LIB.parent))
57
57
 
58
+ from lib.base.hook_utils import load_hook_input
58
59
  from lib.base.utils import eprint, project_dir
59
60
  from lib.context.context_manager import (
60
61
  get_all_contexts,
@@ -301,15 +302,10 @@ def main():
301
302
  and prints system reminder if context is low.
302
303
  """
303
304
  try:
304
- # Read hook input from stdin
305
- input_data = sys.stdin.read().strip()
305
+ # Read hook input using shared utility
306
+ hook_input = load_hook_input()
306
307
 
307
- if not input_data:
308
- return
309
-
310
- try:
311
- hook_input = json.loads(input_data)
312
- except json.JSONDecodeError:
308
+ if not hook_input:
313
309
  return
314
310
 
315
311
  # Always check for mode transitions on implementation tools
@@ -28,6 +28,7 @@ SCRIPT_DIR = Path(__file__).resolve().parent
28
28
  SHARED_LIB = SCRIPT_DIR.parent / "lib"
29
29
  sys.path.insert(0, str(SHARED_LIB.parent))
30
30
 
31
+ from lib.base.hook_utils import load_hook_input
31
32
  from lib.base.utils import eprint, project_dir
32
33
  from lib.base.constants import (
33
34
  get_context_plans_dir,
@@ -167,17 +168,10 @@ def main():
167
168
  and outputs file suggestions as JSON array.
168
169
  """
169
170
  try:
170
- # Read hook input from stdin
171
- input_data = sys.stdin.read().strip()
171
+ # Read hook input using shared utility
172
+ hook_input = load_hook_input()
172
173
 
173
- if not input_data:
174
- print("[]")
175
- return
176
-
177
- try:
178
- hook_input = json.loads(input_data)
179
- except json.JSONDecodeError:
180
- eprint("[file-suggestion] Failed to parse input JSON")
174
+ if not hook_input:
181
175
  print("[]")
182
176
  return
183
177
 
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env python3
2
+ """SessionStart hook for mode transitions after /clear.
3
+
4
+ This hook fires when a new session starts. It handles the critical transition
5
+ from `pending_implementation` to `implementing` when a session starts after
6
+ /clear with bypass permissions.
7
+
8
+ The flow is:
9
+ 1. User approves plan (ExitPlanMode) -> mode = pending_implementation
10
+ 2. User clicks "yes and clear and bypass permissions"
11
+ 3. SessionStart fires with source="clear" and permission_mode="bypassPermissions"
12
+ 4. This hook transitions mode to "implementing"
13
+
14
+ Without this hook, the mode stays stuck at pending_implementation because
15
+ UserPromptSubmit may not receive the correct permission_mode after /clear.
16
+
17
+ Hook input:
18
+ {
19
+ "hook_event_name": "SessionStart",
20
+ "session_id": "abc123",
21
+ "source": "clear", # or "startup", "resume", "compact"
22
+ "permission_mode": "bypassPermissions",
23
+ "model": "...",
24
+ ...
25
+ }
26
+ """
27
+ import sys
28
+ from pathlib import Path
29
+
30
+ # Add parent directories to path for imports
31
+ SCRIPT_DIR = Path(__file__).resolve().parent
32
+ SHARED_LIB = SCRIPT_DIR.parent / "lib"
33
+ sys.path.insert(0, str(SHARED_LIB.parent))
34
+
35
+ from lib.base.hook_utils import load_hook_input
36
+ from lib.base.utils import eprint, project_dir
37
+ from lib.context.context_manager import (
38
+ get_all_in_flight_contexts,
39
+ update_plan_status,
40
+ update_context_session_id,
41
+ )
42
+
43
+
44
+ def main():
45
+ """
46
+ Handle mode transitions on session start.
47
+
48
+ When source is "clear" and permission_mode is "bypassPermissions" or "acceptEdits",
49
+ transition any pending_implementation context to implementing.
50
+ """
51
+ try:
52
+ # Read hook input using shared utility
53
+ hook_input = load_hook_input()
54
+
55
+ if not hook_input:
56
+ return
57
+
58
+ source = hook_input.get("source", "unknown")
59
+ permission_mode = hook_input.get("permission_mode", "default")
60
+ session_id = hook_input.get("session_id", "unknown")
61
+ project_root = project_dir(hook_input)
62
+
63
+ eprint(f"[session_start] source={source}, permission_mode={permission_mode}, session={session_id[:8]}...")
64
+
65
+ # Only handle /clear with bypass/accept permissions
66
+ if source != "clear":
67
+ eprint(f"[session_start] Skipping: source is '{source}', not 'clear'")
68
+ return
69
+
70
+ if permission_mode == "plan":
71
+ eprint(f"[session_start] Skipping: permission_mode is 'plan' (in planning mode)")
72
+ return
73
+
74
+ # Find contexts in pending_implementation mode
75
+ in_flight_contexts = get_all_in_flight_contexts(project_root)
76
+ pending_contexts = [
77
+ ctx for ctx in in_flight_contexts
78
+ if ctx.in_flight and ctx.in_flight.mode == "pending_implementation"
79
+ ]
80
+
81
+ if not pending_contexts:
82
+ eprint("[session_start] No pending_implementation contexts found")
83
+ return
84
+
85
+ # Transition each pending context to implementing
86
+ for ctx in pending_contexts:
87
+ eprint(f"[session_start] Transitioning {ctx.id} from pending_implementation to implementing")
88
+ update_plan_status(ctx.id, "implementing", project_root=project_root)
89
+
90
+ # Also bind this session to the context
91
+ update_context_session_id(ctx.id, session_id, project_root)
92
+ eprint(f"[session_start] Bound session {session_id[:8]}... to context {ctx.id}")
93
+
94
+ eprint(f"[session_start] Transitioned {len(pending_contexts)} context(s) to implementing")
95
+
96
+ except Exception as e:
97
+ eprint(f"[session_start] ERROR: {e}")
98
+ import traceback
99
+ eprint(traceback.format_exc())
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()