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.
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/context_enforcer.py +65 -15
- package/dist/templates/_shared/hooks/session_start.py +108 -0
- package/dist/templates/_shared/hooks/task_create_atomicity.py +199 -0
- package/dist/templates/_shared/hooks/task_create_capture.py +2 -2
- package/dist/templates/_shared/hooks/task_update_capture.py +2 -2
- package/dist/templates/_shared/hooks/user_prompt_submit.py +58 -13
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/inference.py +20 -35
- package/dist/templates/_shared/lib/base/stop_words.py +158 -0
- package/dist/templates/_shared/lib/base/utils.py +3 -2
- package/dist/templates/_shared/lib/context/__init__.py +0 -2
- package/dist/templates/_shared/lib/context/__pycache__/__init__.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__/task_sync.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/context_manager.py +2 -2
- package/dist/templates/_shared/lib/context/task_sync.py +5 -82
- package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/persona_questions.py +113 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +13 -27
- package/dist/templates/cc-native/.claude/agents/cc-native/ACCESSIBILITY-TESTER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/ARCHITECT-REVIEWER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/ASSUMPTION-CHAIN-TRACER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/CLARITY-AUDITOR.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/CODE-REVIEWER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/COMPLETENESS-CHECKER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/CONTEXT-EXTRACTOR.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/DEVILS-ADVOCATE.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/FEASIBILITY-ANALYST.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/FRESH-PERSPECTIVE.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/HANDOFF-READINESS.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/HIDDEN-COMPLEXITY-DETECTOR.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/INCENTIVE-MAPPER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/PENETRATION-TESTER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/PERFORMANCE-ENGINEER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/PLAN-ORCHESTRATOR.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/PRECEDENT-FINDER.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/REVERSIBILITY-ANALYST.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/RISK-ASSESSOR.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/SECOND-ORDER-ANALYST.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/SIMPLICITY-GUARDIAN.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/SKEPTIC.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/STAKEHOLDER-ADVOCATE.md +1 -1
- package/dist/templates/cc-native/.claude/agents/cc-native/TRADE-OFF-ILLUMINATOR.md +1 -1
- package/dist/templates/cc-native/.claude/settings.json +21 -0
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +211 -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/cc-native-plan-review.py +48 -9
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +240 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +1 -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/agent.py +1 -0
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +6 -0
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/templates/cc-native/_cc-native/docs/PERMISSION_REQUEST_VERIFICATION.md +0 -147
|
Binary file
|
|
Binary file
|
|
@@ -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(
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
66
|
-
#
|
|
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
|
|
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
|
|
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))
|