aiwcli 0.9.1 → 0.9.2

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 (63) hide show
  1. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  2. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  3. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  4. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  5. package/dist/templates/_shared/hooks/context_enforcer.py +65 -15
  6. package/dist/templates/_shared/hooks/session_start.py +108 -0
  7. package/dist/templates/_shared/hooks/task_create_atomicity.py +199 -0
  8. package/dist/templates/_shared/hooks/task_create_capture.py +2 -2
  9. package/dist/templates/_shared/hooks/task_update_capture.py +2 -2
  10. package/dist/templates/_shared/hooks/user_prompt_submit.py +58 -13
  11. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  12. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  13. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/lib/base/inference.py +20 -35
  15. package/dist/templates/_shared/lib/base/stop_words.py +158 -0
  16. package/dist/templates/_shared/lib/base/utils.py +3 -2
  17. package/dist/templates/_shared/lib/context/__init__.py +0 -2
  18. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  19. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  20. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  21. package/dist/templates/_shared/lib/context/context_manager.py +2 -2
  22. package/dist/templates/_shared/lib/context/task_sync.py +5 -82
  23. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  24. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  25. package/dist/templates/_shared/lib/templates/persona_questions.py +113 -0
  26. package/dist/templates/_shared/lib/templates/plan_context.py +13 -27
  27. package/dist/templates/cc-native/.claude/agents/cc-native/ACCESSIBILITY-TESTER.md +1 -1
  28. package/dist/templates/cc-native/.claude/agents/cc-native/ARCHITECT-REVIEWER.md +1 -1
  29. package/dist/templates/cc-native/.claude/agents/cc-native/ASSUMPTION-CHAIN-TRACER.md +1 -1
  30. package/dist/templates/cc-native/.claude/agents/cc-native/CLARITY-AUDITOR.md +1 -1
  31. package/dist/templates/cc-native/.claude/agents/cc-native/CODE-REVIEWER.md +1 -1
  32. package/dist/templates/cc-native/.claude/agents/cc-native/COMPLETENESS-CHECKER.md +1 -1
  33. package/dist/templates/cc-native/.claude/agents/cc-native/CONTEXT-EXTRACTOR.md +1 -1
  34. package/dist/templates/cc-native/.claude/agents/cc-native/DEVILS-ADVOCATE.md +1 -1
  35. package/dist/templates/cc-native/.claude/agents/cc-native/FEASIBILITY-ANALYST.md +1 -1
  36. package/dist/templates/cc-native/.claude/agents/cc-native/FRESH-PERSPECTIVE.md +1 -1
  37. package/dist/templates/cc-native/.claude/agents/cc-native/HANDOFF-READINESS.md +1 -1
  38. package/dist/templates/cc-native/.claude/agents/cc-native/HIDDEN-COMPLEXITY-DETECTOR.md +1 -1
  39. package/dist/templates/cc-native/.claude/agents/cc-native/INCENTIVE-MAPPER.md +1 -1
  40. package/dist/templates/cc-native/.claude/agents/cc-native/PENETRATION-TESTER.md +1 -1
  41. package/dist/templates/cc-native/.claude/agents/cc-native/PERFORMANCE-ENGINEER.md +1 -1
  42. package/dist/templates/cc-native/.claude/agents/cc-native/PLAN-ORCHESTRATOR.md +1 -1
  43. package/dist/templates/cc-native/.claude/agents/cc-native/PRECEDENT-FINDER.md +1 -1
  44. package/dist/templates/cc-native/.claude/agents/cc-native/REVERSIBILITY-ANALYST.md +1 -1
  45. package/dist/templates/cc-native/.claude/agents/cc-native/RISK-ASSESSOR.md +1 -1
  46. package/dist/templates/cc-native/.claude/agents/cc-native/SECOND-ORDER-ANALYST.md +1 -1
  47. package/dist/templates/cc-native/.claude/agents/cc-native/SIMPLICITY-GUARDIAN.md +1 -1
  48. package/dist/templates/cc-native/.claude/agents/cc-native/SKEPTIC.md +1 -1
  49. package/dist/templates/cc-native/.claude/agents/cc-native/STAKEHOLDER-ADVOCATE.md +1 -1
  50. package/dist/templates/cc-native/.claude/agents/cc-native/TRADE-OFF-ILLUMINATOR.md +1 -1
  51. package/dist/templates/cc-native/.claude/settings.json +21 -0
  52. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +211 -0
  53. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  54. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +48 -9
  55. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +240 -0
  56. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  57. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +1 -0
  58. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  59. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +1 -0
  60. package/dist/templates/cc-native/_cc-native/plan-review.config.json +6 -0
  61. package/oclif.manifest.json +1 -1
  62. package/package.json +1 -1
  63. package/dist/templates/cc-native/_cc-native/docs/PERMISSION_REQUEST_VERIFICATION.md +0 -147
@@ -299,13 +299,18 @@ def format_context_picker_stderr(contexts: List[Context]) -> str:
299
299
  return "\n".join(lines)
300
300
 
301
301
 
302
- def format_command_feedback(ended_contexts: List[Context], selected_context: Optional[Context]) -> str:
302
+ def format_command_feedback(
303
+ ended_contexts: List[Context],
304
+ selected_context: Optional[Context],
305
+ remaining_prompt: Optional[str] = None
306
+ ) -> str:
303
307
  """
304
308
  Format feedback about what context operations were performed.
305
309
 
306
310
  Args:
307
311
  ended_contexts: Contexts that were ended/completed
308
312
  selected_context: Context that was selected (if any)
313
+ remaining_prompt: User's actual request after caret command (if any)
309
314
 
310
315
  Returns:
311
316
  Formatted feedback message
@@ -338,6 +343,14 @@ def format_command_feedback(ended_contexts: List[Context], selected_context: Opt
338
343
  lines.append(f'All work belongs to context "{selected_context.id}".')
339
344
  lines.append("Tasks created with TaskCreate will be persisted to this context.")
340
345
 
346
+ # Add user's actual request if provided after caret command
347
+ if remaining_prompt and remaining_prompt.strip():
348
+ lines.append("")
349
+ lines.append("---")
350
+ lines.append("")
351
+ lines.append("**User's actual request:**")
352
+ lines.append(f"> {remaining_prompt}")
353
+
341
354
  return "\n".join(lines)
342
355
 
343
356
 
@@ -345,7 +358,7 @@ def determine_context(
345
358
  user_prompt: str,
346
359
  project_root: Path = None,
347
360
  session_id: str = None
348
- ) -> Tuple[Optional[str], str, Optional[str]]:
361
+ ) -> Tuple[Optional[str], str, Optional[str], Optional[str]]:
349
362
  """
350
363
  Determine which context this prompt belongs to.
351
364
 
@@ -355,6 +368,7 @@ def determine_context(
355
368
  - method: How context was determined (session_match, in_flight, caret_select,
356
369
  auto_created, single_context, blocked)
357
370
  - output: System reminder to inject, or None
371
+ - remaining_prompt: Actual user request after caret command, or None
358
372
 
359
373
  Raises:
360
374
  BlockRequest: When request should be blocked to show picker to user
@@ -362,7 +376,7 @@ def determine_context(
362
376
  # 0. Skip context creation for internal subprocess calls (orchestrator, agents)
363
377
  if is_internal_call():
364
378
  eprint("[context_enforcer] Skipping: internal subprocess call")
365
- return (None, "skip_internal", None)
379
+ return (None, "skip_internal", None, None)
366
380
 
367
381
  # 1. Check if session already belongs to a context (HIGHEST PRIORITY)
368
382
  # This prevents context switching on subsequent prompts - one context per session
@@ -373,11 +387,20 @@ def determine_context(
373
387
  return (
374
388
  session_context.id,
375
389
  "session_match",
376
- format_active_context_reminder(session_context)
390
+ format_active_context_reminder(session_context),
391
+ None
377
392
  )
378
393
 
379
394
  # 2. Check for bare "^" - show context picker
380
395
  if user_prompt.strip() == "^":
396
+ # Pre-transition: Move any pending_implementation contexts to implementing
397
+ # This ensures they appear selectable when user opens the picker
398
+ in_flight = get_all_in_flight_contexts(project_root)
399
+ for ctx in in_flight:
400
+ if ctx.in_flight and ctx.in_flight.mode == "pending_implementation":
401
+ update_plan_status(ctx.id, "implementing", project_root=project_root)
402
+ eprint(f"[context_enforcer] Pre-transitioned {ctx.id} to implementing (bare caret)")
403
+
381
404
  contexts = get_all_contexts(status="active", project_root=project_root)
382
405
  if not contexts:
383
406
  raise BlockRequest(
@@ -407,7 +430,7 @@ def determine_context(
407
430
 
408
431
  # Don't auto-create for greetings or help commands
409
432
  if any(prompt_lower.startswith(p) or prompt_lower == p for p in skip_patterns):
410
- return (None, "no_context_needed", None)
433
+ return (None, "no_context_needed", None, None)
411
434
 
412
435
  # Auto-create context from prompt
413
436
  try:
@@ -419,11 +442,12 @@ def determine_context(
419
442
  return (
420
443
  new_context.id,
421
444
  "auto_created",
422
- format_context_created(new_context)
445
+ format_context_created(new_context),
446
+ None
423
447
  )
424
448
  except Exception as e:
425
449
  eprint(f"[context_enforcer] Failed to create context: {e}")
426
- return (None, "creation_failed", None)
450
+ return (None, "creation_failed", None, None)
427
451
 
428
452
  elif len(in_flight_contexts) == 1:
429
453
  # Single in-flight context - auto-select it
@@ -431,6 +455,15 @@ def determine_context(
431
455
  mode = ctx.in_flight.mode if ctx.in_flight else "none"
432
456
  eprint(f"[context_enforcer] Auto-selected single in-flight context: {ctx.id} (mode={mode})")
433
457
 
458
+ # Auto-transition pending_implementation to implementing
459
+ # This ensures state updates immediately when context is selected,
460
+ # rather than waiting for _update_in_flight_status() which has conditions
461
+ if mode == "pending_implementation":
462
+ update_plan_status(ctx.id, "implementing", project_root=project_root)
463
+ ctx.in_flight.mode = "implementing" # Update local copy for display
464
+ mode = "implementing" # Update local var for formatter selection
465
+ eprint(f"[context_enforcer] Transitioned {ctx.id} to implementing")
466
+
434
467
  # Use mode-specific formatter for better continuation context
435
468
  if mode == "pending_implementation":
436
469
  output = format_pending_plan_continuation(ctx)
@@ -439,7 +472,7 @@ def determine_context(
439
472
  else:
440
473
  output = format_active_context_reminder(ctx)
441
474
 
442
- return (ctx.id, "auto_selected", output)
475
+ return (ctx.id, "auto_selected", output, None)
443
476
 
444
477
  else:
445
478
  # Multiple in-flight contexts - block and show picker
@@ -455,7 +488,7 @@ def _handle_caret_command(
455
488
  user_prompt: str,
456
489
  contexts: List[Context],
457
490
  project_root: Path
458
- ) -> Tuple[Optional[str], str, Optional[str]]:
491
+ ) -> Tuple[Optional[str], str, Optional[str], Optional[str]]:
459
492
  """
460
493
  Handle explicit caret commands (^E, ^S, ^0, ^N).
461
494
 
@@ -465,7 +498,7 @@ def _handle_caret_command(
465
498
  project_root: Project root directory
466
499
 
467
500
  Returns:
468
- Tuple of (context_id, method, output)
501
+ Tuple of (context_id, method, output, remaining_prompt)
469
502
 
470
503
  Raises:
471
504
  BlockRequest: When command is invalid or selection needed
@@ -505,7 +538,8 @@ def _handle_caret_command(
505
538
  return (
506
539
  new_context.id,
507
540
  "caret_new",
508
- format_context_created(new_context)
541
+ format_context_created(new_context),
542
+ None
509
543
  )
510
544
  except Exception as e:
511
545
  eprint(f"[context_enforcer] Failed to create context: {e}")
@@ -542,7 +576,8 @@ def _handle_caret_command(
542
576
  return (
543
577
  new_context.id,
544
578
  "caret_new",
545
- output
579
+ output,
580
+ None
546
581
  )
547
582
  except Exception as e:
548
583
  eprint(f"[context_enforcer] Failed to create context: {e}")
@@ -552,11 +587,24 @@ def _handle_caret_command(
552
587
  if cmd.select:
553
588
  selected_ctx = contexts[cmd.select - 1] # 1-indexed
554
589
  eprint(f"[context_enforcer] Caret-selected context: {selected_ctx.id}")
555
- output = format_command_feedback(ended_contexts, selected_ctx)
590
+
591
+ # Auto-transition pending_implementation to implementing
592
+ mode = selected_ctx.in_flight.mode if selected_ctx.in_flight else "none"
593
+ if mode == "pending_implementation":
594
+ update_plan_status(selected_ctx.id, "implementing", project_root=project_root)
595
+ selected_ctx.in_flight.mode = "implementing"
596
+ eprint(f"[context_enforcer] Transitioned {selected_ctx.id} to implementing")
597
+
598
+ output = format_command_feedback(
599
+ ended_contexts,
600
+ selected_ctx,
601
+ cmd.remaining_prompt if cmd.remaining_prompt else None
602
+ )
556
603
  return (
557
604
  selected_ctx.id,
558
605
  "caret_select",
559
- output
606
+ output,
607
+ cmd.remaining_prompt if cmd.remaining_prompt else None
560
608
  )
561
609
 
562
610
  # Only ended contexts, no selection - refresh context list and block
@@ -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)
@@ -0,0 +1,108 @@
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 json
28
+ import sys
29
+ from pathlib import Path
30
+
31
+ # Add parent directories to path for imports
32
+ SCRIPT_DIR = Path(__file__).resolve().parent
33
+ SHARED_LIB = SCRIPT_DIR.parent / "lib"
34
+ sys.path.insert(0, str(SHARED_LIB.parent))
35
+
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 from stdin
53
+ input_data = sys.stdin.read().strip()
54
+
55
+ if not input_data:
56
+ return
57
+
58
+ try:
59
+ hook_input = json.loads(input_data)
60
+ except json.JSONDecodeError:
61
+ return
62
+
63
+ source = hook_input.get("source", "unknown")
64
+ permission_mode = hook_input.get("permission_mode", "default")
65
+ session_id = hook_input.get("session_id", "unknown")
66
+ project_root = project_dir(hook_input)
67
+
68
+ eprint(f"[session_start] source={source}, permission_mode={permission_mode}, session={session_id[:8]}...")
69
+
70
+ # Only handle /clear with bypass/accept permissions
71
+ if source != "clear":
72
+ eprint(f"[session_start] Skipping: source is '{source}', not 'clear'")
73
+ return
74
+
75
+ if permission_mode == "plan":
76
+ eprint(f"[session_start] Skipping: permission_mode is 'plan' (in planning mode)")
77
+ return
78
+
79
+ # Find contexts in pending_implementation mode
80
+ in_flight_contexts = get_all_in_flight_contexts(project_root)
81
+ pending_contexts = [
82
+ ctx for ctx in in_flight_contexts
83
+ if ctx.in_flight and ctx.in_flight.mode == "pending_implementation"
84
+ ]
85
+
86
+ if not pending_contexts:
87
+ eprint("[session_start] No pending_implementation contexts found")
88
+ return
89
+
90
+ # Transition each pending context to implementing
91
+ for ctx in pending_contexts:
92
+ eprint(f"[session_start] Transitioning {ctx.id} from pending_implementation to implementing")
93
+ update_plan_status(ctx.id, "implementing", project_root=project_root)
94
+
95
+ # Also bind this session to the context
96
+ update_context_session_id(ctx.id, session_id, project_root)
97
+ eprint(f"[session_start] Bound session {session_id[:8]}... to context {ctx.id}")
98
+
99
+ eprint(f"[session_start] Transitioned {len(pending_contexts)} context(s) to implementing")
100
+
101
+ except Exception as e:
102
+ eprint(f"[session_start] ERROR: {e}")
103
+ import traceback
104
+ eprint(traceback.format_exc())
105
+
106
+
107
+ if __name__ == "__main__":
108
+ main()
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse hook for TaskCreate - assesses atomicity and forkability via inference.
3
+
4
+ Ensures tasks contain sufficient self-contained context for independent execution,
5
+ especially when delegated to subagents with zero conversation history.
6
+
7
+ Non-blocking: Warns but allows creation even if atomicity is poor.
8
+ """
9
+
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ # Path setup
15
+ _hook_dir = Path(__file__).resolve().parent
16
+ _shared_lib = _hook_dir.parent / "lib"
17
+ sys.path.insert(0, str(_shared_lib))
18
+
19
+ from base.utils import eprint
20
+ from base.subprocess_utils import is_internal_call
21
+ from base.inference import inference
22
+
23
+ # Prompt engineered per Prompting/Standards.md:
24
+ # - Markdown-only (no XML)
25
+ # - Positive framing (tell what TO do)
26
+ # - 1-3 clear examples matching desired output
27
+ # - Direct imperative instructions
28
+ # - Explicit JSON output format
29
+
30
+ ASSESSMENT_SYSTEM_PROMPT = """You assess task descriptions for atomicity and forkability.
31
+
32
+ ## Definitions
33
+
34
+ **Atomic Task:** Contains ALL context needed for independent execution without reading prior conversation.
35
+
36
+ **Forkable Task:** Can be delegated to a subagent with ZERO conversation history and still be completed successfully.
37
+
38
+ ## Signs of Non-Atomic Tasks
39
+
40
+ Look for these indicators:
41
+ - Contextual references: "the file above", "as discussed", "the mentioned function", "this bug"
42
+ - Vague descriptions assuming prior knowledge: "fix the bug", "update it", "finish the work"
43
+ - Missing specifics: which file? what function? what expected behavior? what error?
44
+ - Pronouns without antecedents: "it", "they", "the issue" without explicit definition
45
+
46
+ ## Signs of Atomic Tasks
47
+
48
+ Well-specified tasks include:
49
+ - Explicit file paths: "Edit src/utils/parser.py"
50
+ - Specific function names: "Modify the validate_input() function"
51
+ - Clear expected behavior: "Should return 404 when user not found"
52
+ - Complete error context: "TypeError on line 45 when input is None"
53
+
54
+ ## Examples
55
+
56
+ **Example 1: Non-Atomic Task**
57
+ Subject: "Fix the bug"
58
+ Description: "The issue we discussed earlier needs to be resolved"
59
+ Assessment: NOT atomic (no file, no function, no error details, references "discussed earlier")
60
+
61
+ **Example 2: Atomic Task**
62
+ Subject: "Fix null pointer in user lookup"
63
+ Description: "In src/services/user.py, the get_user_by_id() function raises TypeError when user_id is None. Add null check at line 23 that returns None early instead of calling database.query()."
64
+ Assessment: Atomic (file path, function name, specific error, exact fix location, expected behavior)
65
+
66
+ **Example 3: Partially Atomic Task**
67
+ Subject: "Add validation to form"
68
+ Description: "Add email validation to the signup form. Return error message if invalid."
69
+ Assessment: NOT fully atomic (missing: which file contains the form? what validation rules? where to display error?)
70
+
71
+ ## Output Format
72
+
73
+ Respond with valid JSON only:
74
+ {
75
+ "atomic": true/false,
76
+ "forkable": true/false,
77
+ "issues": ["specific issue 1", "specific issue 2"],
78
+ "recommendation": "brief actionable suggestion if issues exist, or 'Task is well-specified' if good"
79
+ }"""
80
+
81
+ ASSESSMENT_USER_TEMPLATE = """Assess this task for atomicity and forkability:
82
+
83
+ **Subject:** {subject}
84
+
85
+ **Description:** {description}
86
+
87
+ Evaluate whether a subagent with zero prior context could execute this task successfully."""
88
+
89
+
90
+ def main() -> int:
91
+ # Skip internal calls (prevents recursion from orchestrator/inference)
92
+ if is_internal_call():
93
+ return 0
94
+
95
+ try:
96
+ payload = json.load(sys.stdin)
97
+ except json.JSONDecodeError:
98
+ return 0
99
+
100
+ # Only process TaskCreate
101
+ if payload.get("tool_name") != "TaskCreate":
102
+ return 0
103
+
104
+ tool_input = payload.get("tool_input", {})
105
+ subject = tool_input.get("subject", "")
106
+ description = tool_input.get("description", "")
107
+
108
+ # Skip very short tasks (likely intentionally brief or simple acknowledgments)
109
+ if len(description.strip()) < 15:
110
+ return 0
111
+
112
+ # Call inference to assess atomicity and forkability
113
+ try:
114
+ result = inference(
115
+ system_prompt=ASSESSMENT_SYSTEM_PROMPT,
116
+ user_prompt=ASSESSMENT_USER_TEMPLATE.format(
117
+ subject=subject,
118
+ description=description
119
+ ),
120
+ level="fast", # Use Haiku for minimal latency (~1-2s)
121
+ timeout=12, # Allow up to 12s for inference
122
+ )
123
+
124
+ if not result.success:
125
+ eprint(f"[task-create-atomicity] Inference failed: {result.error}")
126
+ return 0 # Non-blocking on failure
127
+
128
+ # Parse JSON response
129
+ try:
130
+ # Handle potential markdown code blocks in response
131
+ output = result.output.strip()
132
+ if output.startswith("```"):
133
+ # Extract JSON from code block
134
+ lines = output.split("\n")
135
+ json_lines = []
136
+ in_block = False
137
+ for line in lines:
138
+ if line.startswith("```") and not in_block:
139
+ in_block = True
140
+ continue
141
+ elif line.startswith("```") and in_block:
142
+ break
143
+ elif in_block:
144
+ json_lines.append(line)
145
+ output = "\n".join(json_lines)
146
+
147
+ assessment = json.loads(output)
148
+ except json.JSONDecodeError:
149
+ eprint(f"[task-create-atomicity] Failed to parse inference response: {result.output[:100]}")
150
+ return 0
151
+
152
+ # Extract assessment fields
153
+ atomic = assessment.get("atomic", True)
154
+ forkable = assessment.get("forkable", True)
155
+ issues = assessment.get("issues", [])
156
+ recommendation = assessment.get("recommendation", "")
157
+
158
+ # Build context message based on assessment
159
+ if atomic and forkable:
160
+ # Task is good - minimal positive feedback
161
+ context_msg = "Task Assessment: Well-specified and forkable."
162
+ else:
163
+ # Task has issues - inject detailed warning
164
+ status_parts = []
165
+ if not atomic:
166
+ status_parts.append("NOT ATOMIC")
167
+ if not forkable:
168
+ status_parts.append("NOT FORKABLE")
169
+
170
+ issues_text = "\n".join(f"- {issue}" for issue in issues) if issues else "- See recommendation below"
171
+
172
+ context_msg = f"""**TASK ATOMICITY WARNING** ({', '.join(status_parts)})
173
+
174
+ This task may lack sufficient context for independent execution by a subagent.
175
+
176
+ **Issues detected:**
177
+ {issues_text}
178
+
179
+ **Recommendation:** {recommendation}
180
+
181
+ Consider adding specific file paths, function names, expected behaviors, or error details before creating this task."""
182
+
183
+ # Output hook response with additionalContext
184
+ out = {
185
+ "hookSpecificOutput": {
186
+ "hookEventName": "PreToolUse",
187
+ "additionalContext": context_msg
188
+ }
189
+ }
190
+ print(json.dumps(out, ensure_ascii=False))
191
+ return 0
192
+
193
+ except Exception as e:
194
+ eprint(f"[task-create-atomicity] Error: {e}")
195
+ return 0 # Non-blocking on error
196
+
197
+
198
+ if __name__ == "__main__":
199
+ raise SystemExit(main())
@@ -125,10 +125,10 @@ def main() -> int:
125
125
  eprint("[task_create_capture] Invalid tool_input: not a dict")
126
126
  return 0
127
127
 
128
- # Check for skip_persistence flag (used during hydration to avoid duplicates)
128
+ # Check for skip_persistence flag (for programmatic task creation)
129
129
  metadata = tool_input.get("metadata", {})
130
130
  if isinstance(metadata, dict) and metadata.get("skip_persistence"):
131
- eprint("[task_create_capture] Skipping persistence (hydration mode)")
131
+ eprint("[task_create_capture] Skipping persistence (skip_persistence flag set)")
132
132
  return 0
133
133
 
134
134
  # Extract tool response (contains task ID assigned by Claude)
@@ -151,10 +151,10 @@ def main() -> int:
151
151
  eprint("[task_update_capture] Invalid tool_input: not a dict")
152
152
  return 0
153
153
 
154
- # Check for skip_persistence flag (used during hydration to avoid duplicates)
154
+ # Check for skip_persistence flag (for programmatic task updates)
155
155
  metadata = tool_input.get("metadata", {})
156
156
  if isinstance(metadata, dict) and metadata.get("skip_persistence"):
157
- eprint("[task_update_capture] Skipping persistence (hydration mode)")
157
+ eprint("[task_update_capture] Skipping persistence (skip_persistence flag set)")
158
158
  return 0
159
159
 
160
160
  # Get project root and session ID
@@ -36,12 +36,50 @@ from lib.context.context_manager import (
36
36
  get_context,
37
37
  get_context_by_session_id,
38
38
  )
39
- from lib.context.task_sync import generate_hydration_instructions
40
39
 
41
40
  # Import the enforcement module
42
41
  from hooks.context_enforcer import determine_context, BlockRequest
43
42
 
44
43
 
44
+ def format_claudemd_reminder() -> str:
45
+ """Generate reminder to update directory-specific CLAUDE.md files."""
46
+ return """
47
+ ## CLAUDE.md Decision Capture
48
+
49
+ When implementing changes, consider whether this work involves decisions with non-obvious rationale. If so, update or create a CLAUDE.md in the relevant directory.
50
+
51
+ **When to update CLAUDE.md:**
52
+ - Architectural choices (why this pattern over alternatives)
53
+ - Non-obvious constraints (why something MUST be done a certain way)
54
+ - Learned patterns (discovered issues that future work should avoid)
55
+ - Integration decisions (why components connect this way)
56
+ - Workarounds (temporary solutions with context on the underlying issue)
57
+
58
+ **What to capture (use this format):**
59
+
60
+ ```markdown
61
+ ## [Topic]
62
+
63
+ **Decision:** [What was decided]
64
+ **Rationale:** [Why this approach was chosen]
65
+ **Constraint:** [What breaks if this changes]
66
+ ```
67
+
68
+ **Directory-specific:** Place CLAUDE.md in the directory closest to the affected code. If no CLAUDE.md exists, create one with a descriptive header.
69
+
70
+ **Example new CLAUDE.md:**
71
+
72
+ ```markdown
73
+ # Component Name
74
+
75
+ Development decisions and patterns for this component.
76
+
77
+ ## [First Decision Topic]
78
+ ...
79
+ ```
80
+ """
81
+
82
+
45
83
  def _update_in_flight_status(context_id: str, hook_input: dict, project_root: Path) -> None:
46
84
  """
47
85
  Update context in-flight status based on permission mode.
@@ -62,11 +100,12 @@ def _update_in_flight_status(context_id: str, hook_input: dict, project_root: Pa
62
100
  if current_mode != "planning":
63
101
  update_plan_status(context_id, "planning", project_root=project_root)
64
102
  eprint(f"[user_prompt_submit] Set status to 'planning'")
65
- elif permission_mode in ["acceptEdits", "bypassPermissions"]:
66
- # Only transition to implementing if we have pending work
103
+ elif permission_mode != "plan":
104
+ # Any non-plan permission mode transitions pending/planning to implementing
105
+ # This includes "default" (after /clear) and "acceptEdits"/"bypassPermissions"
67
106
  if current_mode in ["pending_implementation", "planning"]:
68
107
  update_plan_status(context_id, "implementing", project_root=project_root)
69
- eprint(f"[user_prompt_submit] Set status to 'implementing'")
108
+ eprint(f"[user_prompt_submit] Set status to 'implementing' (permission_mode={permission_mode})")
70
109
 
71
110
 
72
111
  def main():
@@ -94,21 +133,25 @@ def main():
94
133
  session_id = hook_input.get("session_id", "unknown")
95
134
 
96
135
  outputs: List[str] = []
136
+ active_context_id = None # Track context for CLAUDE.md reminder
97
137
 
98
138
  # First-prompt detection: check if session_id is already bound to a context
99
139
  existing_context = get_context_by_session_id(session_id, project_root)
100
140
 
101
141
  if existing_context:
102
142
  # NOT first prompt - session already bound to context
103
- # Skip expensive context detection and task hydration
143
+ # Skip expensive context detection
104
144
  eprint(f"[user_prompt_submit] Session {session_id[:8]}... already bound to {existing_context.id}")
105
145
  # Still update in-flight status based on permission mode
106
146
  _update_in_flight_status(existing_context.id, hook_input, project_root)
147
+ active_context_id = existing_context.id
107
148
  elif user_prompt:
108
- # FIRST prompt - need context detection and potentially task hydration
149
+ # FIRST prompt - need context detection
109
150
  try:
110
- context_id, method, context_output = determine_context(user_prompt, project_root, session_id)
151
+ context_id, method, context_output, remaining_prompt = determine_context(user_prompt, project_root, session_id)
111
152
  eprint(f"[user_prompt_submit] Context: {method} -> {context_id}")
153
+ if remaining_prompt:
154
+ eprint(f"[user_prompt_submit] Actual request: {remaining_prompt[:50]}...")
112
155
 
113
156
  if context_id:
114
157
  # Bind session to context
@@ -117,12 +160,7 @@ def main():
117
160
 
118
161
  # Update in-flight status based on permission mode
119
162
  _update_in_flight_status(context_id, hook_input, project_root)
120
-
121
- # Task hydration - restore pending tasks from events.jsonl
122
- hydration_instructions = generate_hydration_instructions(context_id, project_root)
123
- if hydration_instructions and "No pending tasks" not in hydration_instructions:
124
- outputs.append(hydration_instructions)
125
- eprint(f"[user_prompt_submit] Generated task hydration instructions")
163
+ active_context_id = context_id
126
164
 
127
165
  if context_output:
128
166
  outputs.append(context_output)
@@ -133,6 +171,13 @@ def main():
133
171
  print(e.message, file=sys.stderr)
134
172
  sys.exit(2)
135
173
 
174
+ # Inject CLAUDE.md reminder when in implementing mode
175
+ if active_context_id:
176
+ context = get_context(active_context_id, project_root)
177
+ if context and context.in_flight and context.in_flight.mode == "implementing":
178
+ outputs.append(f"<system-reminder>{format_claudemd_reminder()}</system-reminder>")
179
+ eprint(f"[user_prompt_submit] Injected CLAUDE.md reminder (mode=implementing)")
180
+
136
181
  # Print output
137
182
  if outputs:
138
183
  print("\n\n".join(outputs))