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.
Files changed (43) hide show
  1. plugins/agy/_sequential_hooks/__init__.py +1 -0
  2. plugins/agy/_sequential_hooks/_adapter.py +566 -0
  3. plugins/agy/_sequential_hooks/_doctor.py +413 -0
  4. plugins/claude/_sequential_hooks/__init__.py +1 -0
  5. plugins/claude/_sequential_hooks/_adapter.py +560 -0
  6. plugins/claude/_sequential_hooks/_doctor.py +455 -0
  7. plugins/codex/_sequential_hooks/__init__.py +1 -0
  8. plugins/codex/_sequential_hooks/_adapter.py +466 -0
  9. plugins/codex/_sequential_hooks/_doctor.py +563 -0
  10. sequential_hooks/__init__.py +3 -0
  11. sequential_hooks/__main__.py +5 -0
  12. sequential_hooks/_arguments.py +222 -0
  13. sequential_hooks/_cleanup.py +88 -0
  14. sequential_hooks/_cli.py +440 -0
  15. sequential_hooks/_containment/__init__.py +410 -0
  16. sequential_hooks/_containment/_posix.py +236 -0
  17. sequential_hooks/_containment/_uncontained.py +239 -0
  18. sequential_hooks/_containment/_windows.py +853 -0
  19. sequential_hooks/_containment/_windows_api.py +570 -0
  20. sequential_hooks/_containment/_windows_launcher.py +189 -0
  21. sequential_hooks/_diagnostics.py +265 -0
  22. sequential_hooks/_doctor/__init__.py +342 -0
  23. sequential_hooks/_doctor/_command.py +469 -0
  24. sequential_hooks/_doctor/_common.py +448 -0
  25. sequential_hooks/_doctor/_types.py +94 -0
  26. sequential_hooks/_downstream.py +181 -0
  27. sequential_hooks/_executable.py +187 -0
  28. sequential_hooks/_executor.py +825 -0
  29. sequential_hooks/_registry.py +103 -0
  30. sequential_hooks/_runner.py +48 -0
  31. sequential_hooks/_types.py +125 -0
  32. sequential_hooks/hosts/__init__.py +130 -0
  33. sequential_hooks/hosts/_contract.py +434 -0
  34. sequential_hooks/hosts/_inspection.py +78 -0
  35. sequential_hooks/hosts/_json.py +150 -0
  36. sequential_hooks/hosts/_records.py +273 -0
  37. sequential_hooks/hosts/_skeleton.py +1326 -0
  38. sequential_hooks/py.typed +0 -0
  39. sequential_hooks-0.1.0.dist-info/METADATA +90 -0
  40. sequential_hooks-0.1.0.dist-info/RECORD +43 -0
  41. sequential_hooks-0.1.0.dist-info/WHEEL +4 -0
  42. sequential_hooks-0.1.0.dist-info/entry_points.txt +7 -0
  43. 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