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.
- sequential_hooks-0.1.1/PKG-INFO +119 -0
- sequential_hooks-0.1.1/README.md +100 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/README.md +2 -2
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/_sequential_hooks/_adapter.py +5 -26
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/_sequential_hooks/_doctor.py +18 -16
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/tests/test_agy_adapter.py +30 -30
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/tests/test_agy_doctor.py +13 -13
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/README.md +3 -3
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/_sequential_hooks/_adapter.py +7 -21
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/_sequential_hooks/_doctor.py +33 -17
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/tests/test_claude_adapter.py +145 -124
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/tests/test_claude_doctor.py +94 -88
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/README.md +6 -2
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/_sequential_hooks/_adapter.py +5 -21
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/tests/test_codex_adapter.py +55 -68
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/tests/test_codex_doctor.py +9 -12
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/pyproject.toml +18 -14
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_cli.py +86 -75
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_containment/_posix.py +4 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_containment/_uncontained.py +2 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_containment/_windows.py +143 -135
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_doctor/__init__.py +18 -13
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_doctor/_command.py +2 -7
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_doctor/_common.py +37 -49
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_doctor/_types.py +2 -24
- sequential_hooks-0.1.1/src/sequential_hooks/_downstream.py +44 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/hosts/_contract.py +112 -108
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/hosts/_skeleton.py +247 -240
- sequential_hooks-0.1.0/PKG-INFO +0 -90
- sequential_hooks-0.1.0/README.md +0 -71
- sequential_hooks-0.1.0/src/sequential_hooks/_downstream.py +0 -181
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/.gitignore +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/LICENSE +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/_build.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/_sequential_hooks/__init__.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/adapter.toml +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/tests/fixtures/agy_post.json +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/agy/tests/fixtures/agy_pre.json +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/_sequential_hooks/__init__.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/adapter.toml +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/tests/fixtures/claude_post.json +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/claude/tests/fixtures/claude_pre.json +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/_sequential_hooks/__init__.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/_sequential_hooks/_doctor.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/adapter.toml +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/tests/fixtures/codex_post.json +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/plugins/codex/tests/fixtures/codex_pre.json +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/__init__.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/__main__.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_arguments.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_cleanup.py +4 -4
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_containment/__init__.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_containment/_windows_api.py +66 -66
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_containment/_windows_launcher.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_diagnostics.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_executable.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_executor.py +3 -3
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_registry.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_runner.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/_types.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/hosts/__init__.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/hosts/_inspection.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/hosts/_json.py +0 -0
- {sequential_hooks-0.1.0 → sequential_hooks-0.1.1}/src/sequential_hooks/hosts/_records.py +101 -101
- {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.
|
|
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`.
|
|
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
|
|
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
|
-
) ->
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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[
|
|
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
|
|
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:
|
|
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('
|
|
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=
|
|
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:
|
|
303
|
-
groups:
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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(
|
|
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[
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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 =
|
|
27
|
-
|
|
28
|
-
(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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:
|
|
39
|
-
|
|
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
|
|
4
|
-
|
|
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
|
-
|
|
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).
|