sequential-hooks 0.1.0__py3-none-any.whl
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.
- plugins/agy/_sequential_hooks/__init__.py +1 -0
- plugins/agy/_sequential_hooks/_adapter.py +566 -0
- plugins/agy/_sequential_hooks/_doctor.py +413 -0
- plugins/claude/_sequential_hooks/__init__.py +1 -0
- plugins/claude/_sequential_hooks/_adapter.py +560 -0
- plugins/claude/_sequential_hooks/_doctor.py +455 -0
- plugins/codex/_sequential_hooks/__init__.py +1 -0
- plugins/codex/_sequential_hooks/_adapter.py +466 -0
- plugins/codex/_sequential_hooks/_doctor.py +563 -0
- sequential_hooks/__init__.py +3 -0
- sequential_hooks/__main__.py +5 -0
- sequential_hooks/_arguments.py +222 -0
- sequential_hooks/_cleanup.py +88 -0
- sequential_hooks/_cli.py +440 -0
- sequential_hooks/_containment/__init__.py +410 -0
- sequential_hooks/_containment/_posix.py +236 -0
- sequential_hooks/_containment/_uncontained.py +239 -0
- sequential_hooks/_containment/_windows.py +853 -0
- sequential_hooks/_containment/_windows_api.py +570 -0
- sequential_hooks/_containment/_windows_launcher.py +189 -0
- sequential_hooks/_diagnostics.py +265 -0
- sequential_hooks/_doctor/__init__.py +342 -0
- sequential_hooks/_doctor/_command.py +469 -0
- sequential_hooks/_doctor/_common.py +448 -0
- sequential_hooks/_doctor/_types.py +94 -0
- sequential_hooks/_downstream.py +181 -0
- sequential_hooks/_executable.py +187 -0
- sequential_hooks/_executor.py +825 -0
- sequential_hooks/_registry.py +103 -0
- sequential_hooks/_runner.py +48 -0
- sequential_hooks/_types.py +125 -0
- sequential_hooks/hosts/__init__.py +130 -0
- sequential_hooks/hosts/_contract.py +434 -0
- sequential_hooks/hosts/_inspection.py +78 -0
- sequential_hooks/hosts/_json.py +150 -0
- sequential_hooks/hosts/_records.py +273 -0
- sequential_hooks/hosts/_skeleton.py +1326 -0
- sequential_hooks/py.typed +0 -0
- sequential_hooks-0.1.0.dist-info/METADATA +90 -0
- sequential_hooks-0.1.0.dist-info/RECORD +43 -0
- sequential_hooks-0.1.0.dist-info/WHEEL +4 -0
- sequential_hooks-0.1.0.dist-info/entry_points.txt +7 -0
- sequential_hooks-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# pyright: reportPrivateUsage=false
|
|
2
|
+
|
|
3
|
+
"""Parse validated wrapper arguments into hook invocations."""
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Callable, Iterable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from sequential_hooks._types import Step
|
|
10
|
+
|
|
11
|
+
_MAX_STEP_COUNT = 3
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class HookInvocation:
|
|
16
|
+
"""Describe one validated hook-mode invocation.
|
|
17
|
+
|
|
18
|
+
Agent and event names are literal strings; the selected host validates them
|
|
19
|
+
after it loads.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
host: Selected coding-agent name.
|
|
23
|
+
event: Explicit `--event` value, otherwise `None`.
|
|
24
|
+
step_timeout: Positive finite per-step timeout in seconds, if set.
|
|
25
|
+
allow_on_failure: Whether approved pre-tool failures may continue.
|
|
26
|
+
steps: One to three literal child commands in execution order.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
host: Selected coding-agent name.
|
|
30
|
+
event: Explicit `--event` value, otherwise `None`.
|
|
31
|
+
step_timeout: Positive finite per-step timeout in seconds, if set.
|
|
32
|
+
allow_on_failure: Whether approved pre-tool failures may continue.
|
|
33
|
+
steps: One to three literal child commands in execution order.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
host: str
|
|
37
|
+
event: str | None
|
|
38
|
+
step_timeout: float | None
|
|
39
|
+
allow_on_failure: bool
|
|
40
|
+
steps: tuple[Step, ...]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class InvocationError(ValueError):
|
|
44
|
+
"""Report invalid syntax with safely recognized agent context.
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
host: Coding-agent name given before the error, if any.
|
|
48
|
+
event: Event name given before the error, if any.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
host: str | None
|
|
52
|
+
event: str | None
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
message: str,
|
|
57
|
+
*,
|
|
58
|
+
host: str | None = None,
|
|
59
|
+
event: str | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Retain invalid syntax details and recognized host context.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
message: Safe diagnostic text.
|
|
65
|
+
host: Coding-agent name given before the error, if any.
|
|
66
|
+
event: Event name given before the error, if any.
|
|
67
|
+
"""
|
|
68
|
+
super().__init__(message)
|
|
69
|
+
self.host = host
|
|
70
|
+
self.event = event
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class _ParseState:
|
|
75
|
+
"""Retain safely recognized wrapper option state."""
|
|
76
|
+
|
|
77
|
+
_host: str | None = None
|
|
78
|
+
_event: str | None = None
|
|
79
|
+
_step_timeout: float | None = None
|
|
80
|
+
_allow_on_failure: bool = False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _error(message: str, host: str | None, event: str | None) -> InvocationError:
|
|
84
|
+
"""Build an error carrying only safely recognized context."""
|
|
85
|
+
return InvocationError(message, host=host, event=event)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _consume_allow_on_failure(state: _ParseState, index: int) -> int:
|
|
89
|
+
"""Consume one non-repeating failure override."""
|
|
90
|
+
if state._allow_on_failure:
|
|
91
|
+
raise _error(
|
|
92
|
+
'--allow-on-failure may be provided only once',
|
|
93
|
+
state._host,
|
|
94
|
+
state._event,
|
|
95
|
+
)
|
|
96
|
+
state._allow_on_failure = True
|
|
97
|
+
return index + 1
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _consume_timeout(tokens: tuple[str, ...], state: _ParseState, index: int) -> int:
|
|
101
|
+
"""Consume one positive finite step timeout."""
|
|
102
|
+
if state._step_timeout is not None:
|
|
103
|
+
raise _error('--step-timeout may be provided only once', state._host, state._event)
|
|
104
|
+
if index + 1 >= len(tokens):
|
|
105
|
+
raise _error('--step-timeout requires a value', state._host, state._event)
|
|
106
|
+
value = tokens[index + 1]
|
|
107
|
+
try:
|
|
108
|
+
timeout = float(value)
|
|
109
|
+
except ValueError as error:
|
|
110
|
+
raise _error(
|
|
111
|
+
'--step-timeout must be a positive finite number',
|
|
112
|
+
state._host,
|
|
113
|
+
state._event,
|
|
114
|
+
) from error
|
|
115
|
+
if not math.isfinite(timeout) or timeout <= 0:
|
|
116
|
+
raise _error(
|
|
117
|
+
'--step-timeout must be a positive finite number',
|
|
118
|
+
state._host,
|
|
119
|
+
state._event,
|
|
120
|
+
)
|
|
121
|
+
state._step_timeout = timeout
|
|
122
|
+
return index + 2
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _is_option_like(value: str) -> bool:
|
|
126
|
+
"""Return whether a token cannot be a host or event name."""
|
|
127
|
+
return not value or value.startswith('-')
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _consume_agent(tokens: tuple[str, ...], state: _ParseState, index: int) -> int:
|
|
131
|
+
"""Consume one literal coding-agent name."""
|
|
132
|
+
if state._host is not None:
|
|
133
|
+
raise _error('--agent may be provided only once', state._host, state._event)
|
|
134
|
+
if index + 1 >= len(tokens) or _is_option_like(tokens[index + 1]):
|
|
135
|
+
raise _error('--agent requires a value', None, state._event)
|
|
136
|
+
state._host = tokens[index + 1]
|
|
137
|
+
return index + 2
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _consume_event(tokens: tuple[str, ...], state: _ParseState, index: int) -> int:
|
|
141
|
+
"""Consume one literal event name."""
|
|
142
|
+
if state._event is not None:
|
|
143
|
+
raise _error('--event may be provided only once', state._host, state._event)
|
|
144
|
+
if index + 1 >= len(tokens) or _is_option_like(tokens[index + 1]):
|
|
145
|
+
raise _error('--event requires a value', state._host, None)
|
|
146
|
+
state._event = tokens[index + 1]
|
|
147
|
+
return index + 2
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _parse_steps(
|
|
151
|
+
child_arguments: tuple[str, ...],
|
|
152
|
+
host: str,
|
|
153
|
+
event: str | None,
|
|
154
|
+
) -> tuple[Step, ...]:
|
|
155
|
+
"""Split exact separators into non-empty steps."""
|
|
156
|
+
if not child_arguments:
|
|
157
|
+
raise _error('at least one child command is required', host, event)
|
|
158
|
+
vectors: list[list[str]] = [[]]
|
|
159
|
+
for argument in child_arguments:
|
|
160
|
+
if argument == '--':
|
|
161
|
+
if not vectors[-1]:
|
|
162
|
+
raise _error('child separator creates an empty step', host, event)
|
|
163
|
+
if len(vectors) == _MAX_STEP_COUNT:
|
|
164
|
+
raise _error('at most three child commands are accepted', host, event)
|
|
165
|
+
vectors.append([])
|
|
166
|
+
else:
|
|
167
|
+
vectors[-1].append(argument)
|
|
168
|
+
if not vectors[-1]:
|
|
169
|
+
raise _error('child separator creates an empty step', host, event)
|
|
170
|
+
return tuple(Step(step_index, tuple(vector)) for step_index, vector in enumerate(vectors, 1))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _parse_wrapper_options(tokens: tuple[str, ...]) -> tuple[_ParseState, int]:
|
|
174
|
+
"""Consume wrapper options until the first child token."""
|
|
175
|
+
state = _ParseState()
|
|
176
|
+
consumers: dict[str, Callable[[int], int]] = {
|
|
177
|
+
'-a': lambda index: _consume_agent(tokens, state, index),
|
|
178
|
+
'-e': lambda index: _consume_event(tokens, state, index),
|
|
179
|
+
'--allow-on-failure': lambda index: _consume_allow_on_failure(state, index),
|
|
180
|
+
'--event': lambda index: _consume_event(tokens, state, index),
|
|
181
|
+
'--agent': lambda index: _consume_agent(tokens, state, index),
|
|
182
|
+
'--step-timeout': lambda index: _consume_timeout(tokens, state, index),
|
|
183
|
+
}
|
|
184
|
+
index = 0
|
|
185
|
+
while index < len(tokens) and tokens[index].startswith('-'):
|
|
186
|
+
argument = tokens[index]
|
|
187
|
+
try:
|
|
188
|
+
consumer = consumers[argument]
|
|
189
|
+
except KeyError as error:
|
|
190
|
+
raise _error(
|
|
191
|
+
'unknown wrapper option; use --help for supported syntax',
|
|
192
|
+
state._host,
|
|
193
|
+
state._event,
|
|
194
|
+
) from error
|
|
195
|
+
index = consumer(index)
|
|
196
|
+
return state, index
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def parse_hook_argv(argv: Iterable[str]) -> HookInvocation:
|
|
200
|
+
"""Parse one hook-mode command without reading stdin or loading a host.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
argv: Tokens after the executable name.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Coding-agent name, event option, timeout, failure option, and steps.
|
|
207
|
+
|
|
208
|
+
Raises:
|
|
209
|
+
InvocationError: If wrapper syntax is invalid or `--agent` is missing.
|
|
210
|
+
"""
|
|
211
|
+
tokens = tuple(argv)
|
|
212
|
+
state, child_index = _parse_wrapper_options(tokens)
|
|
213
|
+
if state._host is None:
|
|
214
|
+
raise _error('--agent is required', None, None)
|
|
215
|
+
steps = _parse_steps(tokens[child_index:], state._host, state._event)
|
|
216
|
+
return HookInvocation(
|
|
217
|
+
state._host,
|
|
218
|
+
state._event,
|
|
219
|
+
state._step_timeout,
|
|
220
|
+
state._allow_on_failure,
|
|
221
|
+
steps,
|
|
222
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Retain cleanup failures while attempting every cleanup action."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from types import TracebackType
|
|
10
|
+
from typing import Self
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CleanupState:
|
|
14
|
+
"""Retain the first failure while attempting every cleanup action.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
first_error: First ordinary exception recorded during the pass, or the
|
|
18
|
+
supplied earlier failure.
|
|
19
|
+
|
|
20
|
+
Each instance represents one cleanup pass. Entering returns the same
|
|
21
|
+
cleanup state and resets only the current action result. The context
|
|
22
|
+
manager retains and suppresses ordinary exceptions so later actions can
|
|
23
|
+
run, but it does not suppress other `BaseException` subclasses. Create a
|
|
24
|
+
new instance for each new pass.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, first_error: Exception | None = None) -> None:
|
|
28
|
+
"""Begin one cleanup pass with an optional earlier failure.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
first_error: Failure already selected before this cleanup pass, if
|
|
32
|
+
any.
|
|
33
|
+
"""
|
|
34
|
+
self._attempt_failed = False
|
|
35
|
+
self.first_error = first_error
|
|
36
|
+
|
|
37
|
+
def __enter__(self) -> Self:
|
|
38
|
+
"""Begin one guarded cleanup action.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
This cleanup state.
|
|
42
|
+
"""
|
|
43
|
+
self._attempt_failed = False
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def __exit__(
|
|
47
|
+
self,
|
|
48
|
+
_error_type: type[BaseException] | None,
|
|
49
|
+
error: BaseException | None,
|
|
50
|
+
_traceback: TracebackType | None,
|
|
51
|
+
) -> bool:
|
|
52
|
+
"""Retain and suppress one ordinary cleanup exception.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
_error_type: Raised exception type, if any.
|
|
56
|
+
error: Raised exception, if any.
|
|
57
|
+
_traceback: Raised exception traceback, if any.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Whether the raised exception was retained and suppressed.
|
|
61
|
+
"""
|
|
62
|
+
if isinstance(error, Exception):
|
|
63
|
+
self._attempt_failed = True
|
|
64
|
+
self.record(error)
|
|
65
|
+
return True
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def attempt(self, action: Callable[[], object]) -> bool:
|
|
69
|
+
"""Run one cleanup action and report whether it succeeded.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
action: Cleanup action to attempt.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Whether the action completed without an ordinary exception.
|
|
76
|
+
"""
|
|
77
|
+
with self:
|
|
78
|
+
action()
|
|
79
|
+
return not self._attempt_failed
|
|
80
|
+
|
|
81
|
+
def record(self, error: Exception | None) -> None:
|
|
82
|
+
"""Retain one failure only when none occurred earlier.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
error: Ordinary failure to retain, if any.
|
|
86
|
+
"""
|
|
87
|
+
if self.first_error is None and error is not None:
|
|
88
|
+
self.first_error = error
|