aiwcli 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1248 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +16 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +19 -0
- package/dist/commands/branch.d.ts +45 -0
- package/dist/commands/branch.js +488 -0
- package/dist/commands/clean.d.ts +34 -0
- package/dist/commands/clean.js +186 -0
- package/dist/commands/clear.d.ts +51 -0
- package/dist/commands/clear.js +835 -0
- package/dist/commands/init/index.d.ts +107 -0
- package/dist/commands/init/index.js +565 -0
- package/dist/commands/launch.d.ts +21 -0
- package/dist/commands/launch.js +108 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/base-command.d.ts +114 -0
- package/dist/lib/base-command.js +153 -0
- package/dist/lib/bmad-installer.d.ts +38 -0
- package/dist/lib/bmad-installer.js +145 -0
- package/dist/lib/claude-settings-types.d.ts +102 -0
- package/dist/lib/claude-settings-types.js +5 -0
- package/dist/lib/config.d.ts +25 -0
- package/dist/lib/config.js +46 -0
- package/dist/lib/debug.d.ts +39 -0
- package/dist/lib/debug.js +74 -0
- package/dist/lib/env-compat.d.ts +26 -0
- package/dist/lib/env-compat.js +35 -0
- package/dist/lib/errors.d.ts +126 -0
- package/dist/lib/errors.js +145 -0
- package/dist/lib/generic-merge.d.ts +74 -0
- package/dist/lib/generic-merge.js +105 -0
- package/dist/lib/git/branch.d.ts +67 -0
- package/dist/lib/git/branch.js +155 -0
- package/dist/lib/git/index.d.ts +11 -0
- package/dist/lib/git/index.js +13 -0
- package/dist/lib/git/safety-checks.d.ts +44 -0
- package/dist/lib/git/safety-checks.js +102 -0
- package/dist/lib/git/types.d.ts +31 -0
- package/dist/lib/git/types.js +6 -0
- package/dist/lib/git/worktree.d.ts +67 -0
- package/dist/lib/git/worktree.js +220 -0
- package/dist/lib/gitignore-manager.d.ts +10 -0
- package/dist/lib/gitignore-manager.js +60 -0
- package/dist/lib/hooks-merger.d.ts +28 -0
- package/dist/lib/hooks-merger.js +94 -0
- package/dist/lib/ide-path-resolver.d.ts +102 -0
- package/dist/lib/ide-path-resolver.js +129 -0
- package/dist/lib/index.d.ts +13 -0
- package/dist/lib/index.js +22 -0
- package/dist/lib/output.d.ts +51 -0
- package/dist/lib/output.js +76 -0
- package/dist/lib/paths.d.ts +66 -0
- package/dist/lib/paths.js +136 -0
- package/dist/lib/quiet.d.ts +12 -0
- package/dist/lib/quiet.js +17 -0
- package/dist/lib/settings-hierarchy.d.ts +42 -0
- package/dist/lib/settings-hierarchy.js +105 -0
- package/dist/lib/spawn.d.ts +105 -0
- package/dist/lib/spawn.js +157 -0
- package/dist/lib/spinner.d.ts +19 -0
- package/dist/lib/spinner.js +34 -0
- package/dist/lib/stdin.d.ts +48 -0
- package/dist/lib/stdin.js +60 -0
- package/dist/lib/template-installer.d.ts +92 -0
- package/dist/lib/template-installer.js +375 -0
- package/dist/lib/template-linter.d.ts +49 -0
- package/dist/lib/template-linter.js +173 -0
- package/dist/lib/template-merger.d.ts +47 -0
- package/dist/lib/template-merger.js +173 -0
- package/dist/lib/template-resolver.d.ts +20 -0
- package/dist/lib/template-resolver.js +60 -0
- package/dist/lib/terminal.d.ts +102 -0
- package/dist/lib/terminal.js +245 -0
- package/dist/lib/tty-detection.d.ts +62 -0
- package/dist/lib/tty-detection.js +83 -0
- package/dist/lib/user-utils.d.ts +5 -0
- package/dist/lib/user-utils.js +23 -0
- package/dist/lib/version.d.ts +99 -0
- package/dist/lib/version.js +144 -0
- package/dist/lib/watch-templates.d.ts +6 -0
- package/dist/lib/watch-templates.js +73 -0
- package/dist/lib/windsurf-hooks-hierarchy.d.ts +30 -0
- package/dist/lib/windsurf-hooks-hierarchy.js +66 -0
- package/dist/lib/windsurf-hooks-merger.d.ts +26 -0
- package/dist/lib/windsurf-hooks-merger.js +53 -0
- package/dist/lib/windsurf-hooks-types.d.ts +33 -0
- package/dist/lib/windsurf-hooks-types.js +5 -0
- package/dist/templates/CLAUDE.md +174 -0
- package/dist/templates/_shared/.claude/commands/handoff.md +14 -0
- package/dist/templates/_shared/.claude/settings.json +61 -0
- package/dist/templates/_shared/.codex/workflows/handoff.md +14 -0
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +14 -0
- package/dist/templates/_shared/hooks/__init__.py +16 -0
- package/dist/templates/_shared/hooks/archive_plan.py +270 -0
- package/dist/templates/_shared/hooks/context_enforcer.py +621 -0
- package/dist/templates/_shared/hooks/context_monitor.py +322 -0
- package/dist/templates/_shared/hooks/file-suggestion.py +188 -0
- package/dist/templates/_shared/hooks/task_create_capture.py +194 -0
- package/dist/templates/_shared/hooks/task_update_capture.py +254 -0
- package/dist/templates/_shared/hooks/user_prompt_submit.py +157 -0
- package/dist/templates/_shared/lib/__init__.py +1 -0
- package/dist/templates/_shared/lib/base/__init__.py +49 -0
- package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/atomic_write.py +180 -0
- package/dist/templates/_shared/lib/base/constants.py +299 -0
- package/dist/templates/_shared/lib/base/inference.py +189 -0
- package/dist/templates/_shared/lib/base/utils.py +216 -0
- package/dist/templates/_shared/lib/context/__init__.py +119 -0
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/cache.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__/event_log.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/cache.py +446 -0
- package/dist/templates/_shared/lib/context/context_manager.py +1171 -0
- package/dist/templates/_shared/lib/context/discovery.py +486 -0
- package/dist/templates/_shared/lib/context/event_log.py +308 -0
- package/dist/templates/_shared/lib/context/plan_archive.py +247 -0
- package/dist/templates/_shared/lib/context/task_sync.py +367 -0
- package/dist/templates/_shared/lib/handoff/__init__.py +22 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +307 -0
- package/dist/templates/_shared/lib/templates/README.md +215 -0
- package/dist/templates/_shared/lib/templates/__init__.py +40 -0
- package/dist/templates/_shared/lib/templates/formatters.py +147 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +119 -0
- package/dist/templates/_shared/scripts/save_handoff.py +99 -0
- package/dist/templates/_shared/workflows/handoff.md +212 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/ACCESSIBILITY-TESTER.md +80 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/ARCHITECT-REVIEWER.md +75 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/ASSUMPTION-CHAIN-TRACER.md +239 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/CLARITY-AUDITOR.md +109 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/CODE-REVIEWER.md +71 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/COMPLETENESS-CHECKER.md +104 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/CONTEXT-EXTRACTOR.md +93 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/DEVILS-ADVOCATE.md +223 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/DOCUMENTATION-REVIEWER.md +73 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/FEASIBILITY-ANALYST.md +93 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/FRESH-PERSPECTIVE.md +103 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/HANDOFF-READINESS.md +145 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/HIDDEN-COMPLEXITY-DETECTOR.md +248 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/INCENTIVE-MAPPER.md +235 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PENETRATION-TESTER.md +80 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PERFORMANCE-ENGINEER.md +76 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PLAN-ORCHESTRATOR.md +141 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/PRECEDENT-FINDER.md +240 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/REVERSIBILITY-ANALYST.md +211 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/RISK-ASSESSOR.md +101 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/SECOND-ORDER-ANALYST.md +197 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/SIMPLICITY-GUARDIAN.md +97 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/SKEPTIC.md +349 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/STAKEHOLDER-ADVOCATE.md +106 -0
- package/dist/templates/cc-native/.claude/agents/cc-native/TRADE-OFF-ILLUMINATOR.md +205 -0
- package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +8 -0
- package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -0
- package/dist/templates/cc-native/.claude/settings.json +119 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +8 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -0
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -0
- package/dist/templates/cc-native/CC-NATIVE-README.md +192 -0
- package/dist/templates/cc-native/MIGRATION.md +86 -0
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +331 -0
- package/dist/templates/cc-native/_cc-native/docs/PERMISSION_REQUEST_VERIFICATION.md +147 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +150 -0
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +746 -0
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +339 -0
- package/dist/templates/cc-native/_cc-native/lib/__init__.py +57 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/async_archive.py +68 -0
- package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +98 -0
- package/dist/templates/cc-native/_cc-native/lib/constants.py +45 -0
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +273 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +28 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +164 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +89 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +119 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +103 -0
- package/dist/templates/cc-native/_cc-native/lib/state.py +251 -0
- package/dist/templates/cc-native/_cc-native/lib/utils.py +830 -0
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +76 -0
- package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +151 -0
- package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +134 -0
- package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -0
- package/dist/types/exit-codes.d.ts +11 -0
- package/dist/types/exit-codes.js +10 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +7 -0
- package/oclif.manifest.json +405 -0
- package/package.json +109 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Context enforcer hook - ensures all work happens within a named context.
|
|
3
|
+
|
|
4
|
+
This hook runs on UserPromptSubmit to determine the active context.
|
|
5
|
+
It enforces that every user interaction happens within a tracked context.
|
|
6
|
+
|
|
7
|
+
Context selection priority:
|
|
8
|
+
1. Session already in context -> Continue in that context (highest priority - prevents switching)
|
|
9
|
+
2. Bare "^" -> Show context picker
|
|
10
|
+
3. Explicit caret commands (^E, ^S, ^0, ^N) -> Process as specified
|
|
11
|
+
4. No caret prefix:
|
|
12
|
+
- 0 in-flight contexts -> Auto-create new context from prompt
|
|
13
|
+
- 1 in-flight context -> Auto-select that context
|
|
14
|
+
- Multiple in-flight contexts -> Block and show picker
|
|
15
|
+
|
|
16
|
+
In-flight modes: planning, pending_implementation, implementing, handoff_pending
|
|
17
|
+
|
|
18
|
+
Prefix syntax:
|
|
19
|
+
- ^: Show context picker (bare caret)
|
|
20
|
+
- ^0 <description>: Create new context (description requires 10+ chars)
|
|
21
|
+
- ^1, ^2, etc: Select existing context by number (shorthand for ^S1, ^S2)
|
|
22
|
+
- ^E<N>: End/complete context N (removes from active list)
|
|
23
|
+
- ^E*: End/complete ALL active contexts
|
|
24
|
+
- ^S<N>: Select context N for this session
|
|
25
|
+
- Chaining: ^E1E2S3 means end contexts 1 and 2, then select context 3
|
|
26
|
+
|
|
27
|
+
Hook input (from Claude Code):
|
|
28
|
+
{
|
|
29
|
+
"hook_type": "UserPromptSubmit",
|
|
30
|
+
"prompt": "user's message text",
|
|
31
|
+
"session_id": "abc123",
|
|
32
|
+
...
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Hook output:
|
|
36
|
+
- Exit 0 + stdout: Context selected, continues with system reminder
|
|
37
|
+
- Exit 2 + stderr: Block request, show context picker to user
|
|
38
|
+
"""
|
|
39
|
+
import json
|
|
40
|
+
import re
|
|
41
|
+
import sys
|
|
42
|
+
from dataclasses import dataclass
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
from typing import List, Optional, Tuple
|
|
45
|
+
|
|
46
|
+
# Add parent directories to path for imports
|
|
47
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
48
|
+
SHARED_LIB = SCRIPT_DIR.parent / "lib"
|
|
49
|
+
sys.path.insert(0, str(SHARED_LIB.parent))
|
|
50
|
+
|
|
51
|
+
from lib.context.context_manager import (
|
|
52
|
+
Context,
|
|
53
|
+
get_all_contexts,
|
|
54
|
+
get_all_in_flight_contexts,
|
|
55
|
+
create_context_from_prompt,
|
|
56
|
+
get_context_by_session_id,
|
|
57
|
+
complete_context,
|
|
58
|
+
update_plan_status,
|
|
59
|
+
)
|
|
60
|
+
from lib.context.discovery import (
|
|
61
|
+
get_in_flight_context,
|
|
62
|
+
format_active_context_reminder,
|
|
63
|
+
format_context_created,
|
|
64
|
+
format_handoff_continuation,
|
|
65
|
+
format_pending_plan_continuation,
|
|
66
|
+
format_implementation_continuation,
|
|
67
|
+
_format_relative_time,
|
|
68
|
+
)
|
|
69
|
+
from lib.templates.formatters import get_mode_display
|
|
70
|
+
from lib.base.utils import eprint, project_dir
|
|
71
|
+
|
|
72
|
+
# Minimum characters required for new context description
|
|
73
|
+
MIN_NEW_CONTEXT_CHARS = 10
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class CaretCommand:
|
|
78
|
+
"""Parsed caret command result."""
|
|
79
|
+
ends: List[int] # Context numbers to end (1-indexed)
|
|
80
|
+
select: Optional[int] # Context number to select (1-indexed), None if not specified
|
|
81
|
+
new_context_desc: Optional[str] # Description for new context (^0)
|
|
82
|
+
remaining_prompt: str # The remaining prompt after the command
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def parse_chained_caret(prompt: str, contexts: List["Context"]) -> Tuple[Optional[CaretCommand], Optional[str]]:
|
|
86
|
+
"""
|
|
87
|
+
Parse chained caret commands from user prompt.
|
|
88
|
+
|
|
89
|
+
Syntax:
|
|
90
|
+
- ^E<N>: End context N
|
|
91
|
+
- ^E<N>+: End context N and all after (e.g., ^E2+ ends 2, 3, 4, ...)
|
|
92
|
+
- ^E*: End ALL contexts
|
|
93
|
+
- ^S<N>: Select context N
|
|
94
|
+
- ^0 <desc>: Create new context (special case)
|
|
95
|
+
- ^<N>: Shorthand for ^S<N> (backwards compat)
|
|
96
|
+
- Chain: ^E1E2S3 means end 1, end 2, select 3
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Tuple of:
|
|
100
|
+
- CaretCommand with parsed actions, or None if no caret prefix
|
|
101
|
+
- Error message if syntax is invalid, or None
|
|
102
|
+
"""
|
|
103
|
+
if not prompt.startswith("^"):
|
|
104
|
+
return None, None
|
|
105
|
+
|
|
106
|
+
# Find where the command ends and the remaining prompt begins
|
|
107
|
+
# Command is everything until first whitespace after ^
|
|
108
|
+
match = re.match(r'^\^(\S+)(?:\s+(.*))?$', prompt, re.DOTALL)
|
|
109
|
+
if not match:
|
|
110
|
+
return None, "Invalid prefix. Use ^E<N> to end, ^S<N> to select, or ^0 <desc> for new context."
|
|
111
|
+
|
|
112
|
+
command_str = match.group(1)
|
|
113
|
+
remaining = (match.group(2) or "").strip()
|
|
114
|
+
|
|
115
|
+
# Handle backwards compat: ^N where N is just a number (shorthand for ^SN)
|
|
116
|
+
if command_str.isdigit():
|
|
117
|
+
num = int(command_str)
|
|
118
|
+
if num == 0:
|
|
119
|
+
# ^0 <description> - create new context
|
|
120
|
+
if len(remaining) < MIN_NEW_CONTEXT_CHARS:
|
|
121
|
+
return None, (
|
|
122
|
+
f"Please provide a longer description for your new context.\n"
|
|
123
|
+
f"Your description '{remaining}' is only {len(remaining)} characters.\n"
|
|
124
|
+
f"Minimum required: {MIN_NEW_CONTEXT_CHARS} characters.\n"
|
|
125
|
+
f"Example: ^0 implement user authentication with JWT tokens"
|
|
126
|
+
)
|
|
127
|
+
return CaretCommand(ends=[], select=None, new_context_desc=remaining, remaining_prompt=""), None
|
|
128
|
+
else:
|
|
129
|
+
# ^N - shorthand for select context N
|
|
130
|
+
if num < 1 or num > len(contexts):
|
|
131
|
+
if len(contexts) == 0:
|
|
132
|
+
return None, "No existing contexts. Use ^0 <description> to create a new one."
|
|
133
|
+
return None, f"Invalid selection. Choose 1-{len(contexts)} for existing contexts, or ^0 for new."
|
|
134
|
+
# Validate context is in "implementing" mode
|
|
135
|
+
ctx = contexts[num - 1]
|
|
136
|
+
if not ctx.in_flight or ctx.in_flight.mode != "implementing":
|
|
137
|
+
mode = ctx.in_flight.mode if ctx.in_flight else "none"
|
|
138
|
+
return None, (
|
|
139
|
+
f"Cannot select context {num} ({ctx.id}) - mode is '{mode}'.\n"
|
|
140
|
+
f"Only contexts in 'implementing' mode can be selected.\n"
|
|
141
|
+
f"Use ^E{num} to end this context, or ^0 <desc> to create a new one."
|
|
142
|
+
)
|
|
143
|
+
return CaretCommand(ends=[], select=num, new_context_desc=None, remaining_prompt=remaining), None
|
|
144
|
+
|
|
145
|
+
# Parse chained commands: E<N>, S<N>, etc.
|
|
146
|
+
ends = []
|
|
147
|
+
select = None
|
|
148
|
+
pos = 0
|
|
149
|
+
|
|
150
|
+
while pos < len(command_str):
|
|
151
|
+
if command_str[pos].upper() == 'E':
|
|
152
|
+
# End command
|
|
153
|
+
pos += 1
|
|
154
|
+
# Check for wildcard (E*) - end all contexts
|
|
155
|
+
if pos < len(command_str) and command_str[pos] == '*':
|
|
156
|
+
pos += 1
|
|
157
|
+
if len(contexts) == 0:
|
|
158
|
+
return None, "No contexts to end."
|
|
159
|
+
# Add all context numbers to ends list
|
|
160
|
+
for i in range(1, len(contexts) + 1):
|
|
161
|
+
if i not in ends:
|
|
162
|
+
ends.append(i)
|
|
163
|
+
else:
|
|
164
|
+
# Read number
|
|
165
|
+
num_start = pos
|
|
166
|
+
while pos < len(command_str) and command_str[pos].isdigit():
|
|
167
|
+
pos += 1
|
|
168
|
+
if num_start == pos:
|
|
169
|
+
return None, f"Expected number or '*' after 'E' at position {num_start + 1}"
|
|
170
|
+
num = int(command_str[num_start:pos])
|
|
171
|
+
if num < 1 or num > len(contexts):
|
|
172
|
+
if len(contexts) == 0:
|
|
173
|
+
return None, "No contexts to end."
|
|
174
|
+
return None, f"Context ^E{num} invalid. Choose 1-{len(contexts)}."
|
|
175
|
+
|
|
176
|
+
# Check for + suffix meaning "this and all after"
|
|
177
|
+
if pos < len(command_str) and command_str[pos] == '+':
|
|
178
|
+
pos += 1
|
|
179
|
+
# Add num and all higher numbers (older contexts)
|
|
180
|
+
for i in range(num, len(contexts) + 1):
|
|
181
|
+
if i not in ends:
|
|
182
|
+
ends.append(i)
|
|
183
|
+
else:
|
|
184
|
+
ends.append(num)
|
|
185
|
+
|
|
186
|
+
elif command_str[pos].upper() == 'S':
|
|
187
|
+
# Select command
|
|
188
|
+
pos += 1
|
|
189
|
+
# Read number
|
|
190
|
+
num_start = pos
|
|
191
|
+
while pos < len(command_str) and command_str[pos].isdigit():
|
|
192
|
+
pos += 1
|
|
193
|
+
if num_start == pos:
|
|
194
|
+
return None, f"Expected number after 'S' at position {num_start + 1}"
|
|
195
|
+
num = int(command_str[num_start:pos])
|
|
196
|
+
if num < 1 or num > len(contexts):
|
|
197
|
+
if len(contexts) == 0:
|
|
198
|
+
return None, "No contexts to select."
|
|
199
|
+
return None, f"Context ^S{num} invalid. Choose 1-{len(contexts)}."
|
|
200
|
+
# Validate context is in "implementing" mode
|
|
201
|
+
ctx = contexts[num - 1]
|
|
202
|
+
if not ctx.in_flight or ctx.in_flight.mode != "implementing":
|
|
203
|
+
mode = ctx.in_flight.mode if ctx.in_flight else "none"
|
|
204
|
+
return None, (
|
|
205
|
+
f"Cannot select context {num} ({ctx.id}) - mode is '{mode}'.\n"
|
|
206
|
+
f"Only contexts in 'implementing' mode can be selected.\n"
|
|
207
|
+
f"Use ^E{num} to end this context, or ^0 <desc> to create a new one."
|
|
208
|
+
)
|
|
209
|
+
# Only first S counts
|
|
210
|
+
if select is None:
|
|
211
|
+
select = num
|
|
212
|
+
|
|
213
|
+
else:
|
|
214
|
+
return None, (
|
|
215
|
+
f"Invalid command '{command_str[pos]}' at position {pos + 1}.\n"
|
|
216
|
+
f"Use E<N> to end, E<N>+ to end N and after, E* to end all, S<N> to select.\n"
|
|
217
|
+
f"Example: ^E1S2 (end 1, select 2), ^E2+ (end 2 and older), ^E* (end all)"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Validate: can't select a context that's being ended
|
|
221
|
+
if select is not None and select in ends:
|
|
222
|
+
return None, f"Cannot select context {select} because it's being ended."
|
|
223
|
+
|
|
224
|
+
return CaretCommand(ends=ends, select=select, new_context_desc=None, remaining_prompt=remaining), None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class BlockRequest(Exception):
|
|
228
|
+
"""Raised when the request should be blocked with a message to the user."""
|
|
229
|
+
def __init__(self, message: str):
|
|
230
|
+
self.message = message
|
|
231
|
+
super().__init__(message)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def format_context_picker_stderr(contexts: List[Context]) -> str:
|
|
235
|
+
"""
|
|
236
|
+
Format context picker for stderr output (visible to user when blocking).
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
contexts: Available contexts to choose from
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Formatted picker message
|
|
243
|
+
"""
|
|
244
|
+
lines = [
|
|
245
|
+
"",
|
|
246
|
+
"+----------------------------------------------------------------+",
|
|
247
|
+
"| CONTEXT SELECTION REQUIRED |",
|
|
248
|
+
"+----------------------------------------------------------------+",
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
implementing_count = 0
|
|
252
|
+
for i, ctx in enumerate(contexts, 1):
|
|
253
|
+
time_str = _format_relative_time(ctx.last_active)
|
|
254
|
+
|
|
255
|
+
# Check if context is in implementing mode (selectable)
|
|
256
|
+
is_implementing = ctx.in_flight and ctx.in_flight.mode == "implementing"
|
|
257
|
+
if is_implementing:
|
|
258
|
+
implementing_count += 1
|
|
259
|
+
|
|
260
|
+
# Add status indicator for in-flight work
|
|
261
|
+
status = ""
|
|
262
|
+
if ctx.in_flight and ctx.in_flight.mode != "none":
|
|
263
|
+
mode_display = get_mode_display(ctx.in_flight.mode)
|
|
264
|
+
if mode_display:
|
|
265
|
+
status = f" {mode_display}"
|
|
266
|
+
|
|
267
|
+
# Truncate summary for display
|
|
268
|
+
summary = ctx.summary[:45] + "..." if len(ctx.summary) > 48 else ctx.summary
|
|
269
|
+
|
|
270
|
+
# Show selectable indicator
|
|
271
|
+
selectable = " [selectable]" if is_implementing else " [end only]"
|
|
272
|
+
lines.append(f"| ^{i} {ctx.id}{status}{selectable}")
|
|
273
|
+
lines.append(f"| {summary}")
|
|
274
|
+
lines.append(f"| [{time_str}]")
|
|
275
|
+
lines.append("|")
|
|
276
|
+
|
|
277
|
+
lines.extend([
|
|
278
|
+
"+----------------------------------------------------------------+",
|
|
279
|
+
"| Usage: |",
|
|
280
|
+
"| ^S<N> - Select context (implementing only) |",
|
|
281
|
+
"| ^E<N> - End/complete context |",
|
|
282
|
+
"| ^E<N>+ - End context N and all after |",
|
|
283
|
+
"| ^E* - End ALL contexts |",
|
|
284
|
+
"| ^E1E2S3 - End #1 and #2, select #3 |",
|
|
285
|
+
"| ^0 work description - Create new context (10+ chars) |",
|
|
286
|
+
"+----------------------------------------------------------------+",
|
|
287
|
+
])
|
|
288
|
+
|
|
289
|
+
if implementing_count == 0:
|
|
290
|
+
lines.extend([
|
|
291
|
+
"| NOTE: No contexts in 'implementing' mode. |",
|
|
292
|
+
"| Use ^E<N> to end old contexts, then ^0 to create new. |",
|
|
293
|
+
"+----------------------------------------------------------------+",
|
|
294
|
+
])
|
|
295
|
+
|
|
296
|
+
lines.append("")
|
|
297
|
+
|
|
298
|
+
return "\n".join(lines)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def format_command_feedback(ended_contexts: List[Context], selected_context: Optional[Context]) -> str:
|
|
302
|
+
"""
|
|
303
|
+
Format feedback about what context operations were performed.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
ended_contexts: Contexts that were ended/completed
|
|
307
|
+
selected_context: Context that was selected (if any)
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Formatted feedback message
|
|
311
|
+
"""
|
|
312
|
+
lines = []
|
|
313
|
+
|
|
314
|
+
if ended_contexts:
|
|
315
|
+
lines.append("## Contexts Ended")
|
|
316
|
+
lines.append("")
|
|
317
|
+
for ctx in ended_contexts:
|
|
318
|
+
lines.append(f"- **{ctx.id}**: {ctx.summary[:50]}{'...' if len(ctx.summary) > 50 else ''}")
|
|
319
|
+
lines.append("")
|
|
320
|
+
|
|
321
|
+
if selected_context:
|
|
322
|
+
lines.append(f"## Active Context: {selected_context.id}")
|
|
323
|
+
lines.append("")
|
|
324
|
+
lines.append(f"**Summary:** {selected_context.summary}")
|
|
325
|
+
|
|
326
|
+
# Build mode display
|
|
327
|
+
mode_display = "Active"
|
|
328
|
+
if selected_context.in_flight and selected_context.in_flight.mode != "none":
|
|
329
|
+
mode_str = get_mode_display(selected_context.in_flight.mode)
|
|
330
|
+
if mode_str:
|
|
331
|
+
mode_display = mode_str.strip("[]")
|
|
332
|
+
|
|
333
|
+
time_str = _format_relative_time(selected_context.last_active)
|
|
334
|
+
lines.append(f"**Mode:** {mode_display}")
|
|
335
|
+
lines.append(f"**Last Active:** {time_str}")
|
|
336
|
+
lines.append("")
|
|
337
|
+
lines.append(f'All work belongs to context "{selected_context.id}".')
|
|
338
|
+
lines.append("Tasks created with TaskCreate will be persisted to this context.")
|
|
339
|
+
|
|
340
|
+
return "\n".join(lines)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def determine_context(
|
|
344
|
+
user_prompt: str,
|
|
345
|
+
project_root: Path = None,
|
|
346
|
+
session_id: str = None
|
|
347
|
+
) -> Tuple[Optional[str], str, Optional[str]]:
|
|
348
|
+
"""
|
|
349
|
+
Determine which context this prompt belongs to.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Tuple of:
|
|
353
|
+
- context_id: Context ID or None if selection needed
|
|
354
|
+
- method: How context was determined (session_match, in_flight, caret_select,
|
|
355
|
+
auto_created, single_context, blocked)
|
|
356
|
+
- output: System reminder to inject, or None
|
|
357
|
+
|
|
358
|
+
Raises:
|
|
359
|
+
BlockRequest: When request should be blocked to show picker to user
|
|
360
|
+
"""
|
|
361
|
+
# 1. Check if session already belongs to a context (HIGHEST PRIORITY)
|
|
362
|
+
# This prevents context switching on subsequent prompts - one context per session
|
|
363
|
+
if session_id:
|
|
364
|
+
session_context = get_context_by_session_id(session_id, project_root)
|
|
365
|
+
if session_context:
|
|
366
|
+
eprint(f"[context_enforcer] Session already in context: {session_context.id}")
|
|
367
|
+
return (
|
|
368
|
+
session_context.id,
|
|
369
|
+
"session_match",
|
|
370
|
+
format_active_context_reminder(session_context)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# 2. Check for bare "^" - show context picker
|
|
374
|
+
if user_prompt.strip() == "^":
|
|
375
|
+
contexts = get_all_contexts(status="active", project_root=project_root)
|
|
376
|
+
if not contexts:
|
|
377
|
+
raise BlockRequest(
|
|
378
|
+
"No contexts exist.\n\n"
|
|
379
|
+
"Just type your task to start a new context.\n"
|
|
380
|
+
"Example: implement user authentication system"
|
|
381
|
+
)
|
|
382
|
+
raise BlockRequest(format_context_picker_stderr(contexts))
|
|
383
|
+
|
|
384
|
+
# 3. Check for explicit caret commands (^E, ^S, ^0, ^N)
|
|
385
|
+
if user_prompt.startswith("^"):
|
|
386
|
+
contexts = get_all_contexts(status="active", project_root=project_root)
|
|
387
|
+
return _handle_caret_command(user_prompt, contexts, project_root)
|
|
388
|
+
|
|
389
|
+
# 4. No caret prefix - check in-flight contexts for auto-selection
|
|
390
|
+
in_flight_contexts = get_all_in_flight_contexts(project_root)
|
|
391
|
+
|
|
392
|
+
if len(in_flight_contexts) == 0:
|
|
393
|
+
# No in-flight work - auto-create new context from prompt
|
|
394
|
+
|
|
395
|
+
# Skip auto-creation for certain prompts that don't represent work
|
|
396
|
+
skip_patterns = [
|
|
397
|
+
"/help", "/clear", "/status", "hello", "hi", "hey",
|
|
398
|
+
"thanks", "thank you", "bye", "goodbye"
|
|
399
|
+
]
|
|
400
|
+
prompt_lower = user_prompt.lower().strip()
|
|
401
|
+
|
|
402
|
+
# Don't auto-create for greetings or help commands
|
|
403
|
+
if any(prompt_lower.startswith(p) or prompt_lower == p for p in skip_patterns):
|
|
404
|
+
return (None, "no_context_needed", None)
|
|
405
|
+
|
|
406
|
+
# Auto-create context from prompt
|
|
407
|
+
try:
|
|
408
|
+
new_context = create_context_from_prompt(user_prompt, project_root)
|
|
409
|
+
# Set to implementing mode so it can be selected
|
|
410
|
+
update_plan_status(new_context.id, "implementing", project_root=project_root)
|
|
411
|
+
new_context.in_flight.mode = "implementing" # Update local copy for display
|
|
412
|
+
eprint(f"[context_enforcer] Auto-created new context: {new_context.id}")
|
|
413
|
+
return (
|
|
414
|
+
new_context.id,
|
|
415
|
+
"auto_created",
|
|
416
|
+
format_context_created(new_context)
|
|
417
|
+
)
|
|
418
|
+
except Exception as e:
|
|
419
|
+
eprint(f"[context_enforcer] Failed to create context: {e}")
|
|
420
|
+
return (None, "creation_failed", None)
|
|
421
|
+
|
|
422
|
+
elif len(in_flight_contexts) == 1:
|
|
423
|
+
# Single in-flight context - auto-select it
|
|
424
|
+
ctx = in_flight_contexts[0]
|
|
425
|
+
mode = ctx.in_flight.mode if ctx.in_flight else "none"
|
|
426
|
+
eprint(f"[context_enforcer] Auto-selected single in-flight context: {ctx.id} (mode={mode})")
|
|
427
|
+
|
|
428
|
+
# Use mode-specific formatter for better continuation context
|
|
429
|
+
if mode == "handoff_pending":
|
|
430
|
+
output = format_handoff_continuation(ctx)
|
|
431
|
+
elif mode == "pending_implementation":
|
|
432
|
+
output = format_pending_plan_continuation(ctx)
|
|
433
|
+
elif mode == "implementing":
|
|
434
|
+
output = format_implementation_continuation(ctx)
|
|
435
|
+
else:
|
|
436
|
+
output = format_active_context_reminder(ctx)
|
|
437
|
+
|
|
438
|
+
return (ctx.id, "auto_selected", output)
|
|
439
|
+
|
|
440
|
+
else:
|
|
441
|
+
# Multiple in-flight contexts - block and show picker
|
|
442
|
+
eprint(f"[context_enforcer] Multiple in-flight contexts ({len(in_flight_contexts)}), showing picker")
|
|
443
|
+
raise BlockRequest(
|
|
444
|
+
f"Multiple contexts have in-flight work ({len(in_flight_contexts)} active).\n"
|
|
445
|
+
"Select one to continue, or use ^ to see all contexts:\n" +
|
|
446
|
+
format_context_picker_stderr(in_flight_contexts)
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _handle_caret_command(
|
|
451
|
+
user_prompt: str,
|
|
452
|
+
contexts: List[Context],
|
|
453
|
+
project_root: Path
|
|
454
|
+
) -> Tuple[Optional[str], str, Optional[str]]:
|
|
455
|
+
"""
|
|
456
|
+
Handle explicit caret commands (^E, ^S, ^0, ^N).
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
user_prompt: User's prompt starting with ^
|
|
460
|
+
contexts: List of active contexts
|
|
461
|
+
project_root: Project root directory
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Tuple of (context_id, method, output)
|
|
465
|
+
|
|
466
|
+
Raises:
|
|
467
|
+
BlockRequest: When command is invalid or selection needed
|
|
468
|
+
"""
|
|
469
|
+
# No contexts case - only ^0 is valid
|
|
470
|
+
if not contexts:
|
|
471
|
+
match = re.match(r'^\^(\S+)(?:\s+(.*))?$', user_prompt, re.DOTALL)
|
|
472
|
+
if not match:
|
|
473
|
+
raise BlockRequest(
|
|
474
|
+
"Invalid prefix. Use ^0 <description> to create a new context.\n"
|
|
475
|
+
"Example: ^0 implement user authentication system"
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
prefix_value = match.group(1)
|
|
479
|
+
remaining = match.group(2) or ""
|
|
480
|
+
|
|
481
|
+
# Must be ^0 for new context
|
|
482
|
+
if not prefix_value.isdigit() or int(prefix_value) != 0:
|
|
483
|
+
raise BlockRequest(
|
|
484
|
+
f"No existing contexts to select. Use ^0 <description> to create a new context.\n"
|
|
485
|
+
f"Example: ^0 implement user authentication system"
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
description = remaining.strip()
|
|
489
|
+
if len(description) < MIN_NEW_CONTEXT_CHARS:
|
|
490
|
+
raise BlockRequest(
|
|
491
|
+
f"Please provide a longer description for your new context.\n"
|
|
492
|
+
f"Your description '{description}' is only {len(description)} characters.\n"
|
|
493
|
+
f"Minimum required: {MIN_NEW_CONTEXT_CHARS} characters.\n"
|
|
494
|
+
f"Example: ^0 implement user authentication with JWT tokens"
|
|
495
|
+
)
|
|
496
|
+
try:
|
|
497
|
+
new_context = create_context_from_prompt(description, project_root)
|
|
498
|
+
update_plan_status(new_context.id, "implementing", project_root=project_root)
|
|
499
|
+
new_context.in_flight.mode = "implementing"
|
|
500
|
+
eprint(f"[context_enforcer] Created context from ^0: {new_context.id}")
|
|
501
|
+
return (
|
|
502
|
+
new_context.id,
|
|
503
|
+
"caret_new",
|
|
504
|
+
format_context_created(new_context)
|
|
505
|
+
)
|
|
506
|
+
except Exception as e:
|
|
507
|
+
eprint(f"[context_enforcer] Failed to create context: {e}")
|
|
508
|
+
raise BlockRequest(f"Failed to create context: {e}")
|
|
509
|
+
|
|
510
|
+
# Parse caret commands
|
|
511
|
+
cmd, error = parse_chained_caret(user_prompt, contexts)
|
|
512
|
+
|
|
513
|
+
if error:
|
|
514
|
+
raise BlockRequest(error + "\n" + format_context_picker_stderr(contexts))
|
|
515
|
+
|
|
516
|
+
if not cmd:
|
|
517
|
+
# Should not happen - user_prompt starts with ^ but didn't parse
|
|
518
|
+
raise BlockRequest(format_context_picker_stderr(contexts))
|
|
519
|
+
|
|
520
|
+
# Process chained commands
|
|
521
|
+
ended_contexts = []
|
|
522
|
+
|
|
523
|
+
# End specified contexts
|
|
524
|
+
for end_num in cmd.ends:
|
|
525
|
+
ctx_to_end = contexts[end_num - 1] # 1-indexed
|
|
526
|
+
complete_context(ctx_to_end.id, project_root)
|
|
527
|
+
ended_contexts.append(ctx_to_end)
|
|
528
|
+
eprint(f"[context_enforcer] Ended context: {ctx_to_end.id}")
|
|
529
|
+
|
|
530
|
+
# Handle new context creation
|
|
531
|
+
if cmd.new_context_desc:
|
|
532
|
+
try:
|
|
533
|
+
new_context = create_context_from_prompt(cmd.new_context_desc, project_root)
|
|
534
|
+
update_plan_status(new_context.id, "implementing", project_root=project_root)
|
|
535
|
+
new_context.in_flight.mode = "implementing"
|
|
536
|
+
eprint(f"[context_enforcer] Created context from ^0: {new_context.id}")
|
|
537
|
+
output = format_command_feedback(ended_contexts, new_context)
|
|
538
|
+
return (
|
|
539
|
+
new_context.id,
|
|
540
|
+
"caret_new",
|
|
541
|
+
output
|
|
542
|
+
)
|
|
543
|
+
except Exception as e:
|
|
544
|
+
eprint(f"[context_enforcer] Failed to create context: {e}")
|
|
545
|
+
raise BlockRequest(f"Failed to create context: {e}")
|
|
546
|
+
|
|
547
|
+
# Handle context selection
|
|
548
|
+
if cmd.select:
|
|
549
|
+
selected_ctx = contexts[cmd.select - 1] # 1-indexed
|
|
550
|
+
eprint(f"[context_enforcer] Caret-selected context: {selected_ctx.id}")
|
|
551
|
+
output = format_command_feedback(ended_contexts, selected_ctx)
|
|
552
|
+
return (
|
|
553
|
+
selected_ctx.id,
|
|
554
|
+
"caret_select",
|
|
555
|
+
output
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
# Only ended contexts, no selection - refresh context list and block
|
|
559
|
+
if ended_contexts:
|
|
560
|
+
remaining_contexts = get_all_contexts(status="active", project_root=project_root)
|
|
561
|
+
feedback = format_command_feedback(ended_contexts, None)
|
|
562
|
+
if not remaining_contexts:
|
|
563
|
+
raise BlockRequest(
|
|
564
|
+
feedback + "\n" +
|
|
565
|
+
"All contexts have been ended. No context selected.\n\n"
|
|
566
|
+
"Just type your task to start a new context.\n"
|
|
567
|
+
"Example: implement user authentication system"
|
|
568
|
+
)
|
|
569
|
+
raise BlockRequest(
|
|
570
|
+
feedback + "\n" +
|
|
571
|
+
"No context selected.\n\n" +
|
|
572
|
+
"Select a context to continue:\n" +
|
|
573
|
+
format_context_picker_stderr(remaining_contexts)
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# Parsed but nothing to do - shouldn't happen
|
|
577
|
+
raise BlockRequest(format_context_picker_stderr(contexts))
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def main():
|
|
581
|
+
"""
|
|
582
|
+
Standalone entry point for testing.
|
|
583
|
+
|
|
584
|
+
In production, use user_prompt_submit.py as the unified entry point.
|
|
585
|
+
"""
|
|
586
|
+
try:
|
|
587
|
+
input_data = sys.stdin.read().strip()
|
|
588
|
+
if not input_data:
|
|
589
|
+
return
|
|
590
|
+
|
|
591
|
+
hook_input = json.loads(input_data)
|
|
592
|
+
user_prompt = hook_input.get("prompt", "")
|
|
593
|
+
if not user_prompt:
|
|
594
|
+
return
|
|
595
|
+
|
|
596
|
+
project_root = project_dir(hook_input)
|
|
597
|
+
|
|
598
|
+
try:
|
|
599
|
+
context_id, method, output = determine_context(user_prompt, project_root)
|
|
600
|
+
eprint(f"[context_enforcer] Method: {method}, Context: {context_id}")
|
|
601
|
+
|
|
602
|
+
if output:
|
|
603
|
+
print(output)
|
|
604
|
+
|
|
605
|
+
except BlockRequest as e:
|
|
606
|
+
# Block the request - print to stderr and exit with code 2
|
|
607
|
+
print(e.message, file=sys.stderr)
|
|
608
|
+
sys.exit(2)
|
|
609
|
+
|
|
610
|
+
except Exception as e:
|
|
611
|
+
eprint(f"[context_enforcer] ERROR: {e}")
|
|
612
|
+
import traceback
|
|
613
|
+
eprint(traceback.format_exc())
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
if __name__ == "__main__":
|
|
617
|
+
main()
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
# Export for use by unified hook
|
|
621
|
+
__all__ = ["determine_context", "BlockRequest"]
|