sequential-hooks 0.1.0__tar.gz → 0.1.1__tar.gz

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 (65) hide show
  1. sequential_hooks-0.1.1/PKG-INFO +119 -0
  2. sequential_hooks-0.1.1/README.md +100 -0
  3. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/README.md +2 -2
  4. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/_sequential_hooks/_adapter.py +5 -26
  5. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/_sequential_hooks/_doctor.py +18 -16
  6. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/tests/test_agy_adapter.py +30 -30
  7. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/tests/test_agy_doctor.py +13 -13
  8. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/README.md +3 -3
  9. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/_sequential_hooks/_adapter.py +7 -21
  10. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/_sequential_hooks/_doctor.py +33 -17
  11. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/tests/test_claude_adapter.py +145 -124
  12. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/tests/test_claude_doctor.py +94 -88
  13. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/README.md +6 -2
  14. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/_sequential_hooks/_adapter.py +5 -21
  15. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/tests/test_codex_adapter.py +55 -68
  16. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/tests/test_codex_doctor.py +9 -12
  17. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/pyproject.toml +18 -14
  18. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_cli.py +86 -75
  19. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_containment/_posix.py +4 -0
  20. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_containment/_uncontained.py +2 -0
  21. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_containment/_windows.py +143 -135
  22. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_doctor/__init__.py +18 -13
  23. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_doctor/_command.py +2 -7
  24. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_doctor/_common.py +37 -49
  25. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_doctor/_types.py +2 -24
  26. sequential_hooks-0.1.1/src/sequential_hooks/_downstream.py +44 -0
  27. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/hosts/_contract.py +112 -108
  28. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/hosts/_skeleton.py +247 -240
  29. sequential_hooks-0.1.0/PKG-INFO +0 -90
  30. sequential_hooks-0.1.0/README.md +0 -71
  31. sequential_hooks-0.1.0/src/sequential_hooks/_downstream.py +0 -181
  32. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/.gitignore +0 -0
  33. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/LICENSE +0 -0
  34. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/_build.py +0 -0
  35. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/_sequential_hooks/__init__.py +0 -0
  36. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/adapter.toml +0 -0
  37. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/tests/fixtures/agy_post.json +0 -0
  38. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/tests/fixtures/agy_pre.json +0 -0
  39. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/_sequential_hooks/__init__.py +0 -0
  40. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/adapter.toml +0 -0
  41. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/tests/fixtures/claude_post.json +0 -0
  42. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/tests/fixtures/claude_pre.json +0 -0
  43. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/_sequential_hooks/__init__.py +0 -0
  44. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/_sequential_hooks/_doctor.py +0 -0
  45. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/adapter.toml +0 -0
  46. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/tests/fixtures/codex_post.json +0 -0
  47. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/tests/fixtures/codex_pre.json +0 -0
  48. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/__init__.py +0 -0
  49. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/__main__.py +0 -0
  50. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_arguments.py +0 -0
  51. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_cleanup.py +4 -4
  52. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_containment/__init__.py +0 -0
  53. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_containment/_windows_api.py +66 -66
  54. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_containment/_windows_launcher.py +0 -0
  55. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_diagnostics.py +0 -0
  56. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_executable.py +0 -0
  57. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_executor.py +3 -3
  58. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_registry.py +0 -0
  59. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_runner.py +0 -0
  60. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_types.py +0 -0
  61. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/hosts/__init__.py +0 -0
  62. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/hosts/_inspection.py +0 -0
  63. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/hosts/_json.py +0 -0
  64. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/hosts/_records.py +101 -101
  65. {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/py.typed +0 -0
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.5
2
+ Name: sequential-hooks
3
+ Version: 0.1.1
4
+ Summary: Run coding-agent hooks in order with agent-specific response handling
5
+ Project-URL: Documentation, https://github.com/dactylo/sequential-hooks/tree/main/docs
6
+ Project-URL: Homepage, https://github.com/dactylo/sequential-hooks
7
+ Project-URL: Issues, https://github.com/dactylo/sequential-hooks/issues
8
+ Project-URL: Repository, https://github.com/dactylo/sequential-hooks
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: coding-agents,hooks,orchestration,sequential
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: 3.15
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.13
18
+ Description-Content-Type: text/markdown
19
+
20
+ # sequential-hooks
21
+
22
+ sequential-hooks runs coding-agent hooks in a strict order. Without a wrapper, a host may start
23
+ several hooks concurrently or in an undocumented order. A validator can then inspect an old tool
24
+ input while another hook is still replacing it. Register one wrapper hook and place the former hook
25
+ commands inside it as ordered steps.
26
+
27
+ ## Installation
28
+
29
+ sequential-hooks requires native CPython 3.13 or newer. Install the current checkout from the
30
+ repository root with [uv](https://docs.astral.sh/uv/):
31
+
32
+ ```text
33
+ uv tool install .
34
+ ```
35
+
36
+ To update an existing installation after pulling changes:
37
+
38
+ ```text
39
+ uv tool install --reinstall .
40
+ ```
41
+
42
+ `make install` and `make reinstall` run the same commands.
43
+
44
+ ## Quick start
45
+
46
+ sequential-hooks has no configuration file. Choose the coding agent that will run the wrapper, then
47
+ add the registration from its setup guide:
48
+
49
+ - [Claude Code](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/plugins/claude/README.md)
50
+ - [Codex](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/plugins/codex/README.md)
51
+ - [Antigravity](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/plugins/agy/README.md)
52
+
53
+ Check [compatibility](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/docs/compatibility.md) for supported hosts and known host limits, and
54
+ [platforms](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/docs/platforms.md) for process handling. The guides use `first-hook`, `second-hook`,
55
+ and `audit-hook` as sample child commands. Replace them with hooks you have installed and want to
56
+ run in order. Move those child hooks into the wrapper and remove their separate registrations, or
57
+ the host may still start duplicate copies concurrently.
58
+
59
+ The wrapper command expects a native host payload on stdin. Configure it as a host hook rather than
60
+ running the hook command interactively. After saving the registration, inspect it with the matching
61
+ read-only report:
62
+
63
+ ```text
64
+ sequential-hooks doctor --agent claude
65
+ sequential-hooks doctor --agent codex
66
+ sequential-hooks doctor --agent agy
67
+ ```
68
+
69
+ Exit 0 means the findings are informational. Exit 1 means at least one warning or error is present.
70
+ Some warnings describe inspection limits and persist after host-side review. The
71
+ [troubleshooting guide](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/docs/troubleshooting.md) explains common findings.
72
+
73
+ Antigravity requires an explicit event in each hook command. An all-neutral Antigravity pre-tool
74
+ chain emits the host's real automatic `allow` decision; it does not preserve the ordinary
75
+ permission prompt. Review the [Antigravity setup guide](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/plugins/agy/README.md) before enabling it.
76
+
77
+ ## How commands compose
78
+
79
+ `-a, --agent` selects the installed host adapter for a coding agent. The built-in names are
80
+ `claude`, `codex`, and `agy`; a third-party adapter installed with
81
+ `uv tool install . --with PACKAGE` is selected by its own name. Nothing is inferred from the
82
+ payload. See [adding a host](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/docs/adding-a-host.md).
83
+
84
+ `-h, --help` prints help for wrapper mode; `doctor -h` and `doctor --help` print doctor help.
85
+
86
+ ```text
87
+ sequential-hooks --agent codex --step-timeout 30 first-hook --agent codex -- second-hook --agent codex
88
+ ```
89
+
90
+ The exact `--` token separates steps and cannot be passed to a step. Each child receives its own
91
+ literal arguments, including its own `--agent`; wrapper options are not inherited by child
92
+ commands.
93
+
94
+ ## Documentation
95
+
96
+ | If you want to... | Read |
97
+ | ---------------------------------------------------------- | ----------------------------------------------------------- |
98
+ | Understand ordering, replacements, and decisions | [Concepts](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/docs/concepts.md) |
99
+ | Review trust boundaries and pre-permission execution | [Security](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/docs/security.md) |
100
+ | Interpret `doctor` findings or runtime failures | [Troubleshooting](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/docs/troubleshooting.md) |
101
+ | Look up hook grammar, limits, and exit codes | [CLI reference](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/docs/reference/cli.md) |
102
+ | Check accepted input, output, and decisions for each event | [Host protocol reference](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/docs/reference/host-protocols.md) |
103
+ | Check supported hosts and platforms | [Compatibility](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/docs/compatibility.md) |
104
+ | Add support for another coding agent | [Adding a host](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/docs/adding-a-host.md) |
105
+ | Browse every guide | [Documentation map](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/docs/README.md) |
106
+
107
+ ## Development
108
+
109
+ See the [development guide](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/docs/development.md) for the required tools, CI, and dependency locks.
110
+ With those tools installed, run:
111
+
112
+ ```text
113
+ make setup
114
+ make check
115
+ ```
116
+
117
+ ## License
118
+
119
+ sequential-hooks is available under the [MIT License](https://github.com/dactylo/sequential-hooks/blob/v0.1.1/LICENSE).
@@ -0,0 +1,100 @@
1
+ # sequential-hooks
2
+
3
+ sequential-hooks runs coding-agent hooks in a strict order. Without a wrapper, a host may start
4
+ several hooks concurrently or in an undocumented order. A validator can then inspect an old tool
5
+ input while another hook is still replacing it. Register one wrapper hook and place the former hook
6
+ commands inside it as ordered steps.
7
+
8
+ ## Installation
9
+
10
+ sequential-hooks requires native CPython 3.13 or newer. Install the current checkout from the
11
+ repository root with [uv](https://docs.astral.sh/uv/):
12
+
13
+ ```text
14
+ uv tool install .
15
+ ```
16
+
17
+ To update an existing installation after pulling changes:
18
+
19
+ ```text
20
+ uv tool install --reinstall .
21
+ ```
22
+
23
+ `make install` and `make reinstall` run the same commands.
24
+
25
+ ## Quick start
26
+
27
+ sequential-hooks has no configuration file. Choose the coding agent that will run the wrapper, then
28
+ add the registration from its setup guide:
29
+
30
+ - [Claude Code](plugins/claude/README.md)
31
+ - [Codex](plugins/codex/README.md)
32
+ - [Antigravity](plugins/agy/README.md)
33
+
34
+ Check [compatibility](docs/compatibility.md) for supported hosts and known host limits, and
35
+ [platforms](docs/platforms.md) for process handling. The guides use `first-hook`, `second-hook`,
36
+ and `audit-hook` as sample child commands. Replace them with hooks you have installed and want to
37
+ run in order. Move those child hooks into the wrapper and remove their separate registrations, or
38
+ the host may still start duplicate copies concurrently.
39
+
40
+ The wrapper command expects a native host payload on stdin. Configure it as a host hook rather than
41
+ running the hook command interactively. After saving the registration, inspect it with the matching
42
+ read-only report:
43
+
44
+ ```text
45
+ sequential-hooks doctor --agent claude
46
+ sequential-hooks doctor --agent codex
47
+ sequential-hooks doctor --agent agy
48
+ ```
49
+
50
+ Exit 0 means the findings are informational. Exit 1 means at least one warning or error is present.
51
+ Some warnings describe inspection limits and persist after host-side review. The
52
+ [troubleshooting guide](docs/troubleshooting.md) explains common findings.
53
+
54
+ Antigravity requires an explicit event in each hook command. An all-neutral Antigravity pre-tool
55
+ chain emits the host's real automatic `allow` decision; it does not preserve the ordinary
56
+ permission prompt. Review the [Antigravity setup guide](plugins/agy/README.md) before enabling it.
57
+
58
+ ## How commands compose
59
+
60
+ `-a, --agent` selects the installed host adapter for a coding agent. The built-in names are
61
+ `claude`, `codex`, and `agy`; a third-party adapter installed with
62
+ `uv tool install . --with PACKAGE` is selected by its own name. Nothing is inferred from the
63
+ payload. See [adding a host](docs/adding-a-host.md).
64
+
65
+ `-h, --help` prints help for wrapper mode; `doctor -h` and `doctor --help` print doctor help.
66
+
67
+ ```text
68
+ sequential-hooks --agent codex --step-timeout 30 first-hook --agent codex -- second-hook --agent codex
69
+ ```
70
+
71
+ The exact `--` token separates steps and cannot be passed to a step. Each child receives its own
72
+ literal arguments, including its own `--agent`; wrapper options are not inherited by child
73
+ commands.
74
+
75
+ ## Documentation
76
+
77
+ | If you want to... | Read |
78
+ | ---------------------------------------------------------- | ----------------------------------------------------------- |
79
+ | Understand ordering, replacements, and decisions | [Concepts](docs/concepts.md) |
80
+ | Review trust boundaries and pre-permission execution | [Security](docs/security.md) |
81
+ | Interpret `doctor` findings or runtime failures | [Troubleshooting](docs/troubleshooting.md) |
82
+ | Look up hook grammar, limits, and exit codes | [CLI reference](docs/reference/cli.md) |
83
+ | Check accepted input, output, and decisions for each event | [Host protocol reference](docs/reference/host-protocols.md) |
84
+ | Check supported hosts and platforms | [Compatibility](docs/compatibility.md) |
85
+ | Add support for another coding agent | [Adding a host](docs/adding-a-host.md) |
86
+ | Browse every guide | [Documentation map](docs/README.md) |
87
+
88
+ ## Development
89
+
90
+ See the [development guide](docs/development.md) for the required tools, CI, and dependency locks.
91
+ With those tools installed, run:
92
+
93
+ ```text
94
+ make setup
95
+ make check
96
+ ```
97
+
98
+ ## License
99
+
100
+ sequential-hooks is available under the [MIT License](LICENSE).
@@ -1,7 +1,7 @@
1
1
  # Antigravity
2
2
 
3
3
  The Antigravity adapter chains `PreToolUse` and `PostToolUse` hooks. Antigravity may invoke an
4
- exact post-tool matcher for extra callbacks whose `toolCall` is explicitly null. `sequential-hooks`
4
+ exact post-tool matcher for extra callbacks whose `toolCall` is explicitly null. sequential-hooks
5
5
  treats that exact shape as inapplicable, starts no child step, and returns the native empty
6
6
  response.
7
7
 
@@ -10,7 +10,7 @@ Antigravity CLI 1.1.13 documents five native hook events: `PreToolUse`, `PostToo
10
10
  `PostToolUse`.
11
11
 
12
12
  Antigravity configuration may appear in `~/.gemini/config/hooks.json` or project
13
- `.agents/hooks.json`. `sequential-hooks` has no configuration file.
13
+ `.agents/hooks.json`. sequential-hooks has no configuration file.
14
14
 
15
15
  Official reference: [Antigravity hooks](https://antigravity.google/docs/hooks).
16
16
 
@@ -66,17 +66,8 @@ class _RankedContribution:
66
66
  _step: Step
67
67
 
68
68
 
69
- class AgyAdapter:
70
- """Compose child results for one validated Antigravity event.
71
-
72
- Child results are consumed in step order and composed into an event-
73
- specific final response. The original strict JSON input remains the input
74
- for every child.
75
-
76
- Attributes:
77
- event: Validated pre-tool or post-tool event.
78
- should_run_steps: Whether the invocation should execute child steps.
79
- """
69
+ class _AgyAdapter:
70
+ """Compose child results for one validated Antigravity event."""
80
71
 
81
72
  def __init__(
82
73
  self,
@@ -87,19 +78,7 @@ class AgyAdapter:
87
78
  *,
88
79
  allow_on_failure: bool,
89
80
  ) -> None:
90
- """Validate native input and initialize one Antigravity event adapter.
91
-
92
- Args:
93
- event: Exact CLI event selected by the caller.
94
- raw_input: Original strict JSON bytes received from Antigravity.
95
- payload: Decoded strict JSON object matching `raw_input`.
96
- diagnostics: Developer-tier diagnostic sink.
97
- allow_on_failure: Whether approved pre-tool failures may continue.
98
-
99
- Raises:
100
- HostInputError: If required Antigravity input is missing, has the
101
- wrong type, or is invalid.
102
- """
81
+ """Validate native input and initialize one Antigravity event adapter."""
103
82
  if event not in _EVENTS:
104
83
  raise HostInputError('Antigravity input has an unsupported event')
105
84
  step_index = payload.get('stepIdx')
@@ -487,7 +466,7 @@ class AgyHost:
487
466
  diagnostics: Diagnostics,
488
467
  *,
489
468
  allow_on_failure: bool,
490
- ) -> AgyAdapter:
469
+ ) -> _AgyAdapter:
491
470
  """Validate native input and construct the adapter for one invocation.
492
471
 
493
472
  Args:
@@ -503,7 +482,7 @@ class AgyHost:
503
482
  Raises:
504
483
  HostInputError: If the payload is not valid input for `event`.
505
484
  """
506
- return AgyAdapter(
485
+ return _AgyAdapter(
507
486
  event,
508
487
  raw_input,
509
488
  payload,
@@ -9,8 +9,10 @@ from sequential_hooks.hosts import (
9
9
  Finding,
10
10
  FindingSeverity,
11
11
  InspectedCommand,
12
+ JsonObject,
12
13
  JsonObjectExpectedError,
13
14
  JsonProtocolError,
15
+ JsonValue,
14
16
  LaunchSemantics,
15
17
  Registration,
16
18
  SourceKind,
@@ -177,12 +179,12 @@ def _finding(
177
179
 
178
180
  def _handler_validation_finding(
179
181
  source: ConfigSource,
180
- handler: object,
182
+ handler: JsonValue,
181
183
  ) -> Finding | None:
182
184
  """Return the first structural problem in one handler."""
183
185
  if not isinstance(handler, dict):
184
186
  return _finding(source, 'agy_hook_malformed', 'contains a malformed hook handler')
185
- handler_data = cast('dict[str, object]', handler)
187
+ handler_data = handler
186
188
  handler_type = handler_data.get('type', 'command')
187
189
  if handler_type != 'command':
188
190
  return _finding(
@@ -198,7 +200,7 @@ def _handler_validation_finding(
198
200
  )
199
201
  if 'args' in handler_data and (
200
202
  not isinstance(raw_args := handler_data['args'], list)
201
- or not all(isinstance(argument, str) for argument in cast('list[object]', raw_args))
203
+ or not all(isinstance(argument, str) for argument in raw_args)
202
204
  ):
203
205
  return _finding(
204
206
  source,
@@ -219,7 +221,7 @@ def _inspection_limitations() -> tuple[str, ...]:
219
221
  return _LIMITATIONS
220
222
 
221
223
 
222
- def _load_source(source: ConfigSource) -> tuple[dict[str, object] | None, Finding | None]:
224
+ def _load_source(source: ConfigSource) -> tuple[JsonObject | None, Finding | None]:
223
225
  """Load one strict JSON source without exposing its content."""
224
226
  try:
225
227
  text = source.path.read_text(encoding='utf-8')
@@ -237,14 +239,14 @@ def _load_source(source: ConfigSource) -> tuple[dict[str, object] | None, Findin
237
239
  return None, _finding(source, 'agy_settings_malformed', 'must contain a JSON object')
238
240
  except JsonProtocolError:
239
241
  return None, _finding(source, 'agy_json_malformed', 'contains malformed JSON')
240
- return cast('dict[str, object]', loaded), None
242
+ return loaded, None
241
243
 
242
244
 
243
245
  def _registration(
244
246
  source: ConfigSource,
245
247
  event: str,
246
248
  matcher: str | None,
247
- handler: object,
249
+ handler: JsonValue,
248
250
  *,
249
251
  enabled: bool | None,
250
252
  ) -> tuple[Registration | None, list[Finding]]:
@@ -252,7 +254,7 @@ def _registration(
252
254
  validation_finding = _handler_validation_finding(source, handler)
253
255
  if validation_finding is not None:
254
256
  return None, [validation_finding]
255
- handler_data = cast('dict[str, object]', handler)
257
+ handler_data = cast('JsonObject', handler)
256
258
  raw_args = cast('list[str] | None', handler_data.get('args'))
257
259
  raw_timeout = handler_data.get('timeout')
258
260
  timeout = _DEFAULT_HANDLER_TIMEOUT
@@ -291,7 +293,7 @@ def _registration(
291
293
  windows_launch_semantics=LaunchSemantics.WINDOWS_SHELL,
292
294
  timeout=timeout,
293
295
  enabled=enabled,
294
- type=cast('str', handler_data.get('type', 'command')),
296
+ type='command',
295
297
  configuration_findings=tuple(findings),
296
298
  )
297
299
  return registration, []
@@ -299,17 +301,17 @@ def _registration(
299
301
 
300
302
  def _event_registrations(
301
303
  source: ConfigSource,
302
- event: object,
303
- groups: object,
304
+ event: str,
305
+ groups: JsonValue,
304
306
  *,
305
307
  enabled: bool | None,
306
308
  ) -> tuple[list[Registration], list[Finding]]:
307
309
  """Parse one event layer."""
308
- if not isinstance(event, str) or not isinstance(groups, list):
310
+ if not isinstance(groups, list):
309
311
  return [], [_finding(source, 'agy_event_malformed', 'contains a malformed event')]
310
312
  registrations: list[Registration] = []
311
313
  findings: list[Finding] = []
312
- for group in cast('list[object]', groups):
314
+ for group in groups:
313
315
  if not isinstance(group, dict):
314
316
  findings.append(
315
317
  _finding(
@@ -319,7 +321,7 @@ def _event_registrations(
319
321
  )
320
322
  )
321
323
  continue
322
- group_data = cast('dict[str, object]', group)
324
+ group_data = group
323
325
  matcher = group_data.get('matcher')
324
326
  if 'matcher' in group_data and not isinstance(matcher, str):
325
327
  findings.append(
@@ -336,7 +338,7 @@ def _event_registrations(
336
338
  )
337
339
  )
338
340
  continue
339
- for handler in cast('list[object]', handlers):
341
+ for handler in handlers:
340
342
  registration, handler_findings = _registration(
341
343
  source,
342
344
  event,
@@ -352,7 +354,7 @@ def _event_registrations(
352
354
 
353
355
  def _named_registrations(
354
356
  source: ConfigSource,
355
- loaded: dict[str, object],
357
+ loaded: JsonObject,
356
358
  ) -> tuple[list[Registration], list[Finding]]:
357
359
  """Parse the documented named-hook map."""
358
360
  registrations: list[Registration] = []
@@ -367,7 +369,7 @@ def _named_registrations(
367
369
  )
368
370
  )
369
371
  continue
370
- hook_data = cast('dict[str, object]', named_hook)
372
+ hook_data = named_hook
371
373
  enabled = hook_data.get('enabled', True)
372
374
  if not isinstance(enabled, bool):
373
375
  findings.append(
@@ -9,7 +9,7 @@ from typing import cast
9
9
  import pytest
10
10
 
11
11
  from plugins.agy._sequential_hooks import _adapter as agy_module
12
- from plugins.agy._sequential_hooks._adapter import AgyAdapter, AgyHost
12
+ from plugins.agy._sequential_hooks._adapter import AgyHost, _AgyAdapter
13
13
  from sequential_hooks._diagnostics import Diagnostics
14
14
  from sequential_hooks._executor import _STDOUT_LIMIT_BYTES
15
15
  from sequential_hooks._types import (
@@ -42,7 +42,7 @@ _PRE_RANK = {
42
42
  _UNCONTAINED_WARNING = 'step ran without containment; descendant cleanup is not guaranteed'
43
43
 
44
44
 
45
- class _OutputFailureAdapter(AgyAdapter):
45
+ class _OutputFailureAdapter(_AgyAdapter):
46
46
  def consume_output_failure(
47
47
  self,
48
48
  step: Step,
@@ -65,11 +65,11 @@ def _adapter(
65
65
  raw_input: bytes | None = None,
66
66
  *,
67
67
  allow_on_failure: bool = False,
68
- ) -> tuple[AgyAdapter, io.StringIO]:
68
+ ) -> tuple[_AgyAdapter, io.StringIO]:
69
69
  data = _fixture(event) if raw_input is None else raw_input
70
70
  stream = io.StringIO()
71
71
  return (
72
- AgyAdapter(
72
+ _AgyAdapter(
73
73
  event,
74
74
  data,
75
75
  load_object(data),
@@ -105,7 +105,7 @@ def _output_failure_adapter(
105
105
  )
106
106
 
107
107
 
108
- def _response(adapter: AgyAdapter) -> JsonObject:
108
+ def _response(adapter: _AgyAdapter) -> JsonObject:
109
109
  return load_object(adapter.finalize().stdout)
110
110
 
111
111
 
@@ -131,6 +131,30 @@ def test_all_neutral_chain_emits_canonical_allow() -> None:
131
131
  assert adapter.finalize().stdout == b'{"decision":"allow"}\n'
132
132
 
133
133
 
134
+ @pytest.mark.parametrize('event', ['PreToolUse', 'PostToolUse'])
135
+ def test_configuration_response_is_independent_of_adapter_construction(
136
+ event: str,
137
+ monkeypatch: pytest.MonkeyPatch,
138
+ ) -> None:
139
+ def unavailable(*_args: object, **_kwargs: object) -> None:
140
+ raise AssertionError('configuration response must not construct an adapter')
141
+
142
+ monkeypatch.setattr('plugins.agy._sequential_hooks._adapter._AgyAdapter', unavailable)
143
+ monkeypatch.setattr(AgyHost, 'adapter', unavailable)
144
+ stream = io.StringIO()
145
+ response = AgyHost().configuration_response(
146
+ event, 'invalid configuration', Diagnostics(stream)
147
+ )
148
+ message = '[step 1: configuration]\nspawn: sequential-hooks could not safely run this hook'
149
+ expected = {'decision': 'deny', 'reason': message} if event == 'PreToolUse' else {}
150
+
151
+ assert response.exit_code == 0
152
+ assert json.loads(response.stdout) == expected
153
+ assert response.stdout.endswith(b'\n')
154
+ assert response.stdout.count(b'\n') == 1
155
+ assert 'sequential-hooks could not safely run this hook' in stream.getvalue()
156
+
157
+
134
158
  def test_conflicting_winning_overrides_become_terminal_deny() -> None:
135
159
  adapter, _stream = _adapter('PreToolUse')
136
160
  adapter.consume(
@@ -419,7 +443,7 @@ def test_lone_surrogate_uncontained_failure_restores_exact_prior_state() -> None
419
443
  raw_input = _fixture('PreToolUse')
420
444
  payload = load_object(raw_input)
421
445
  stream = io.StringIO()
422
- adapter = AgyAdapter(
446
+ adapter = _AgyAdapter(
423
447
  'PreToolUse',
424
448
  raw_input,
425
449
  payload,
@@ -1210,27 +1234,3 @@ def test_whitespace_only_stdout_is_neutral(stdout: bytes) -> None:
1210
1234
  assert control == 'continue'
1211
1235
  assert adapter.finalize().stdout == b'{"decision":"allow"}\n'
1212
1236
  assert stream.getvalue() == ''
1213
-
1214
-
1215
- @pytest.mark.parametrize('event', ['PreToolUse', 'PostToolUse'])
1216
- def test_configuration_response_is_independent_of_adapter_construction(
1217
- event: str,
1218
- monkeypatch: pytest.MonkeyPatch,
1219
- ) -> None:
1220
- def unavailable(*_args: object, **_kwargs: object) -> None:
1221
- raise AssertionError('configuration response must not construct an adapter')
1222
-
1223
- monkeypatch.setattr('plugins.agy._sequential_hooks._adapter.AgyAdapter', unavailable)
1224
- monkeypatch.setattr(AgyHost, 'adapter', unavailable)
1225
- stream = io.StringIO()
1226
- response = AgyHost().configuration_response(
1227
- event, 'invalid configuration', Diagnostics(stream)
1228
- )
1229
- message = '[step 1: configuration]\nspawn: sequential-hooks could not safely run this hook'
1230
- expected = {'decision': 'deny', 'reason': message} if event == 'PreToolUse' else {}
1231
-
1232
- assert response.exit_code == 0
1233
- assert json.loads(response.stdout) == expected
1234
- assert response.stdout.endswith(b'\n')
1235
- assert response.stdout.count(b'\n') == 1
1236
- assert 'sequential-hooks could not safely run this hook' in stream.getvalue()
@@ -3,7 +3,6 @@
3
3
  import json
4
4
  import subprocess
5
5
  from pathlib import Path
6
- from typing import cast
7
6
 
8
7
  import pytest
9
8
 
@@ -19,25 +18,22 @@ from sequential_hooks._registry import load_host
19
18
  from sequential_hooks.hosts import (
20
19
  Finding,
21
20
  FindingSeverity,
21
+ JsonValue,
22
22
  LaunchSemantics,
23
23
  SourceKind,
24
24
  )
25
25
 
26
- _MALFORMED_HOOKS_NAME_CASES = cast(
27
- 'tuple[tuple[object, str], ...]',
28
- (
29
- ([], 'agy_named_hook_malformed'),
30
- ({'PreToolUse': {}}, 'agy_event_malformed'),
31
- ({'PreToolUse': [None]}, 'agy_matcher_group_malformed'),
32
- ({'PreToolUse': [{'matcher': None, 'hooks': []}]}, 'agy_matcher_malformed'),
33
- ({'PreToolUse': [{}]}, 'agy_handler_list_malformed'),
34
- ),
26
+ _MALFORMED_HOOKS_NAME_CASES: tuple[tuple[JsonValue, str], ...] = (
27
+ ([], 'agy_named_hook_malformed'),
28
+ ({'PreToolUse': {}}, 'agy_event_malformed'),
29
+ ({'PreToolUse': [None]}, 'agy_matcher_group_malformed'),
30
+ ({'PreToolUse': [{'matcher': None, 'hooks': []}]}, 'agy_matcher_malformed'),
31
+ ({'PreToolUse': [{}]}, 'agy_handler_list_malformed'),
35
32
  )
36
33
 
37
34
 
38
- def _non_global_codes(findings: object) -> list[str]:
39
- typed = cast('tuple[Finding, ...]', findings)
40
- return [finding.code for finding in typed]
35
+ def _non_global_codes(findings: tuple[Finding, ...]) -> list[str]:
36
+ return [finding.code for finding in findings]
41
37
 
42
38
 
43
39
  def _write_json(path: Path, data: object) -> None:
@@ -380,6 +376,7 @@ def test_duplicate_literal_options_remain_uninspectable(
380
376
  assert 'wrong_host' not in codes
381
377
 
382
378
 
379
+ @pytest.mark.usefixtures('distribution_root')
383
380
  def test_dynamic_command_has_no_malformed_wrapper_warning(tmp_path: Path) -> None:
384
381
  _write_hook_named_hooks(
385
382
  tmp_path / '.gemini/config/hooks.json',
@@ -665,6 +662,7 @@ def test_named_hooks_preserve_mixed_source_order(tmp_path: Path) -> None:
665
662
  ]
666
663
 
667
664
 
665
+ @pytest.mark.usefixtures('distribution_root')
668
666
  @pytest.mark.parametrize(
669
667
  ('command', 'event', 'expected_code'),
670
668
  [
@@ -716,6 +714,7 @@ def test_shell_command_checks_registered_wrapper_event(
716
714
  assert expected_code in codes
717
715
 
718
716
 
717
+ @pytest.mark.usefixtures('distribution_root')
719
718
  @pytest.mark.parametrize(
720
719
  ('command', 'expected_code'),
721
720
  [
@@ -769,6 +768,7 @@ def test_shell_command_preserves_safe_host_diagnoses(
769
768
  assert matching[0].correction == 'Use --agent agy.'
770
769
 
771
770
 
771
+ @pytest.mark.usefixtures('distribution_root')
772
772
  def test_stop_wrappers_report_only_unservable_native_event(tmp_path: Path) -> None:
773
773
  source = tmp_path / '.gemini/config/hooks.json'
774
774
  _write_hook_named_hooks(
@@ -1,7 +1,7 @@
1
1
  # Claude Code
2
2
 
3
- Use `sequential-hooks` for Claude Code `PreToolUse` and `PostToolUse` command hooks. Claude Code
4
- owns these local files:
3
+ Use sequential-hooks for Claude Code `PreToolUse` and `PostToolUse` command hooks. Claude Code owns
4
+ these local files:
5
5
 
6
6
  - User: `~/.claude/settings.json`.
7
7
  - Project: `.claude/settings.json`.
@@ -13,7 +13,7 @@ owns these local files:
13
13
  - Windows managed: `C:\Program Files\ClaudeCode\managed-settings.json` and JSON files in
14
14
  `C:\Program Files\ClaudeCode\managed-settings.d\`.
15
15
 
16
- `sequential-hooks` has no configuration file.
16
+ sequential-hooks has no configuration file.
17
17
 
18
18
  Official references: [hooks](https://code.claude.com/docs/en/hooks) and
19
19
  [hooks guide](https://code.claude.com/docs/en/hooks-guide).