claude-dev-env 1.30.1 → 1.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/clean-coder.md +275 -111
- package/agents/code-quality-agent.md +196 -209
- package/bin/install.mjs +81 -0
- package/bin/install.test.mjs +158 -0
- package/bin/install_mypy_ini.mjs +51 -0
- package/bin/install_mypy_ini.test.mjs +121 -0
- package/commands/hook-log-extract.md +70 -0
- package/commands/hook-log-init.md +76 -0
- package/hooks/blocking/code_rules_enforcer.py +5 -3
- package/hooks/blocking/destructive_command_blocker.py +187 -0
- package/hooks/blocking/question_to_user_enforcer.py +140 -0
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +39 -0
- package/hooks/blocking/test_destructive_command_blocker.py +397 -0
- package/hooks/blocking/test_question_to_user_enforcer.py +163 -0
- package/hooks/config/hook_log_extractor_constants.py +221 -0
- package/hooks/config/messages.py +3 -0
- package/hooks/config/test_hook_log_extractor_constants.py +96 -0
- package/hooks/config/test_messages.py +5 -0
- package/hooks/diagnostic/hook_log_extractor.py +907 -0
- package/hooks/diagnostic/hook_log_init.py +202 -0
- package/hooks/diagnostic/hook_log_stop_wrapper.py +84 -0
- package/hooks/diagnostic/migrations/2026-04-25-drop-themes-hook-events.sql +3 -0
- package/hooks/diagnostic/migrations/README.md +77 -0
- package/hooks/diagnostic/queries/block_details_for_hook.sql +26 -0
- package/hooks/diagnostic/queries/blocks_by_category.sql +10 -0
- package/hooks/diagnostic/queries/blocks_by_tool.sql +9 -0
- package/hooks/diagnostic/queries/blocks_last_7_days.sql +11 -0
- package/hooks/diagnostic/queries/top_blockers_last_24_hours.sql +12 -0
- package/hooks/diagnostic/queries/top_blockers_overall.sql +12 -0
- package/hooks/diagnostic/requirements-hook-logs-dev.txt +2 -0
- package/hooks/diagnostic/requirements-hook-logs.txt +1 -0
- package/hooks/diagnostic/schema.sql +51 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +1531 -0
- package/hooks/diagnostic/test_hook_log_init.py +227 -0
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +98 -0
- package/hooks/hooks.json +10 -0
- package/package.json +1 -1
- package/rules/ask-user-question-required.md +44 -0
- package/scripts/config/test_spec_implementer_prompt.py +0 -4
- package/scripts/test_groq_bugteam_spec.py +0 -8
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Initialize the Neon schema for hook-log diagnostics.
|
|
3
|
+
|
|
4
|
+
Verifies required environment variables, opens a psycopg connection,
|
|
5
|
+
applies the idempotent DDL from ``schema.sql``, runs a sentinel
|
|
6
|
+
insert/select/delete round-trip to prove read-write parity, and prints
|
|
7
|
+
a success report with the Neon host, table name, and row count.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from urllib.parse import urlparse
|
|
16
|
+
|
|
17
|
+
if str(Path(__file__).resolve().parent.parent) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import psycopg
|
|
22
|
+
except ImportError:
|
|
23
|
+
psycopg = None
|
|
24
|
+
|
|
25
|
+
from config.hook_log_extractor_constants import (
|
|
26
|
+
CONNECT_TIMEOUT_SECONDS,
|
|
27
|
+
EXIT_CODE_ENVIRONMENT_MISSING,
|
|
28
|
+
EXIT_CODE_SUCCESS,
|
|
29
|
+
HOOK_EVENTS_ROW_COUNT_SQL,
|
|
30
|
+
HOOK_EVENTS_TABLE_NAME,
|
|
31
|
+
MISSING_ENVIRONMENT_VARIABLE_PREFIX,
|
|
32
|
+
MISSING_PSYCOPG_WARNING_LABEL,
|
|
33
|
+
NEON_DATABASE_URL_ENVIRONMENT_VARIABLE,
|
|
34
|
+
NEON_HOST_REPORT_LABEL,
|
|
35
|
+
OUTCOME_INIT_PROBE,
|
|
36
|
+
ROW_COUNT_REPORT_LABEL,
|
|
37
|
+
SCHEMA_RELATIVE_PATH,
|
|
38
|
+
SEMICOLON_SPLIT_TOKEN,
|
|
39
|
+
SENTINEL_DELETE_SQL,
|
|
40
|
+
SENTINEL_HOOK_CATEGORY,
|
|
41
|
+
SENTINEL_HOOK_EVENT,
|
|
42
|
+
SENTINEL_HOOK_NAME,
|
|
43
|
+
SENTINEL_INSERT_FAILURE_MESSAGE,
|
|
44
|
+
SENTINEL_INSERT_SQL,
|
|
45
|
+
SENTINEL_SELECT_FAILURE_MESSAGE,
|
|
46
|
+
SENTINEL_SELECT_SQL,
|
|
47
|
+
SENTINEL_SESSION_ID,
|
|
48
|
+
SENTINEL_SOURCE_LINE_NUMBER,
|
|
49
|
+
SENTINEL_SOURCE_PATH,
|
|
50
|
+
SUCCESS_REPORT_HEADER,
|
|
51
|
+
TABLE_REPORT_LABEL,
|
|
52
|
+
UNKNOWN_HOST_PLACEHOLDER,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MissingPsycopgDependencyError(RuntimeError):
|
|
57
|
+
"""Raised when the psycopg driver is not installed in the interpreter."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def verify_environment_variables() -> list[str]:
|
|
61
|
+
"""Return names of required env vars that are unset; empty list when all present.
|
|
62
|
+
|
|
63
|
+
Only ``NEON_HOOK_LOGS_DATABASE_URL`` is verified here. ``bws run``
|
|
64
|
+
intentionally strips ``BWS_ACCESS_TOKEN`` from the child environment
|
|
65
|
+
to prevent subprocess credential leakage; checking for it inside a
|
|
66
|
+
child process invoked via ``bws run -- python hook_log_init.py``
|
|
67
|
+
would therefore always fail even when the machine is configured
|
|
68
|
+
correctly. The one-time ``setx BWS_ACCESS_TOKEN`` prerequisite is
|
|
69
|
+
documented in ``packages/claude-dev-env/commands/hook-log-init.md``.
|
|
70
|
+
"""
|
|
71
|
+
all_missing_variable_names: list[str] = []
|
|
72
|
+
raw_database_url_value = os.environ.get(NEON_DATABASE_URL_ENVIRONMENT_VARIABLE)
|
|
73
|
+
if raw_database_url_value is None or raw_database_url_value.strip() == "":
|
|
74
|
+
all_missing_variable_names.append(NEON_DATABASE_URL_ENVIRONMENT_VARIABLE)
|
|
75
|
+
return all_missing_variable_names
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _schema_file_path() -> Path:
|
|
79
|
+
"""Return the absolute path to the companion ``schema.sql`` file."""
|
|
80
|
+
return Path(__file__).resolve().parent / SCHEMA_RELATIVE_PATH
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _split_ddl_statements(schema_text: str) -> list[str]:
|
|
84
|
+
"""Split a DDL file on semicolons and drop empty trailing fragments."""
|
|
85
|
+
all_raw_fragments = schema_text.split(SEMICOLON_SPLIT_TOKEN)
|
|
86
|
+
all_trimmed_statements = [
|
|
87
|
+
each_fragment.strip() for each_fragment in all_raw_fragments
|
|
88
|
+
]
|
|
89
|
+
return [
|
|
90
|
+
each_statement for each_statement in all_trimmed_statements if each_statement
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def apply_schema(neon_connection: object) -> None:
|
|
95
|
+
"""Execute the idempotent DDL from ``schema.sql`` on the given connection."""
|
|
96
|
+
schema_text = _schema_file_path().read_text(encoding="utf-8")
|
|
97
|
+
all_ddl_statements = _split_ddl_statements(schema_text)
|
|
98
|
+
with neon_connection.cursor() as neon_cursor:
|
|
99
|
+
for each_statement in all_ddl_statements:
|
|
100
|
+
neon_cursor.execute(each_statement)
|
|
101
|
+
neon_connection.commit()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def run_sentinel_round_trip(neon_connection: object) -> None:
|
|
105
|
+
"""Insert a sentinel row, select it back by id, verify it, then delete it."""
|
|
106
|
+
with neon_connection.cursor() as neon_cursor:
|
|
107
|
+
neon_cursor.execute(
|
|
108
|
+
SENTINEL_INSERT_SQL,
|
|
109
|
+
(
|
|
110
|
+
SENTINEL_SESSION_ID,
|
|
111
|
+
SENTINEL_HOOK_EVENT,
|
|
112
|
+
SENTINEL_HOOK_NAME,
|
|
113
|
+
SENTINEL_HOOK_CATEGORY,
|
|
114
|
+
OUTCOME_INIT_PROBE,
|
|
115
|
+
SENTINEL_SOURCE_PATH,
|
|
116
|
+
SENTINEL_SOURCE_LINE_NUMBER,
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
sentinel_row = neon_cursor.fetchone()
|
|
120
|
+
if sentinel_row is None:
|
|
121
|
+
raise RuntimeError(SENTINEL_INSERT_FAILURE_MESSAGE)
|
|
122
|
+
sentinel_id = sentinel_row[0]
|
|
123
|
+
neon_cursor.execute(SENTINEL_SELECT_SQL, (sentinel_id,))
|
|
124
|
+
fetched_row = neon_cursor.fetchone()
|
|
125
|
+
if fetched_row is None or fetched_row[0] != sentinel_id:
|
|
126
|
+
raise RuntimeError(SENTINEL_SELECT_FAILURE_MESSAGE)
|
|
127
|
+
neon_cursor.execute(SENTINEL_DELETE_SQL, (sentinel_id,))
|
|
128
|
+
neon_connection.commit()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _extract_host_from_database_url(database_url: str) -> str:
|
|
132
|
+
"""Return the hostname portion of a Postgres connection URL."""
|
|
133
|
+
parsed_url = urlparse(database_url)
|
|
134
|
+
return parsed_url.hostname or UNKNOWN_HOST_PLACEHOLDER
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def print_success_report(neon_host: str, table_name: str, row_count: int) -> None:
|
|
138
|
+
"""Print a 4-line success report with Neon host, table name, and row count."""
|
|
139
|
+
print(SUCCESS_REPORT_HEADER)
|
|
140
|
+
print(f"{NEON_HOST_REPORT_LABEL} {neon_host}")
|
|
141
|
+
print(f"{TABLE_REPORT_LABEL} {table_name}")
|
|
142
|
+
print(f"{ROW_COUNT_REPORT_LABEL} {row_count}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def connect_to_neon() -> object:
|
|
146
|
+
"""Open a psycopg v3 connection using the configured Neon database URL.
|
|
147
|
+
|
|
148
|
+
Raises ``MissingPsycopgDependencyError`` when psycopg is not installed
|
|
149
|
+
so the caller can surface a clear actionable message.
|
|
150
|
+
"""
|
|
151
|
+
if psycopg is None:
|
|
152
|
+
raise MissingPsycopgDependencyError(MISSING_PSYCOPG_WARNING_LABEL)
|
|
153
|
+
database_url = os.environ[NEON_DATABASE_URL_ENVIRONMENT_VARIABLE]
|
|
154
|
+
return psycopg.connect(database_url, connect_timeout=CONNECT_TIMEOUT_SECONDS)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _fetch_row_count(neon_connection: object) -> int:
|
|
158
|
+
"""Return the current row count of the ``hook_events`` table."""
|
|
159
|
+
with neon_connection.cursor() as neon_cursor:
|
|
160
|
+
neon_cursor.execute(HOOK_EVENTS_ROW_COUNT_SQL)
|
|
161
|
+
row_count_row = neon_cursor.fetchone()
|
|
162
|
+
return int(row_count_row[0]) if row_count_row else 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _print_missing_environment_variables(all_missing_variable_names: list[str]) -> None:
|
|
166
|
+
"""Write one stderr line per missing environment variable name."""
|
|
167
|
+
for each_missing_name in all_missing_variable_names:
|
|
168
|
+
print(
|
|
169
|
+
f"{MISSING_ENVIRONMENT_VARIABLE_PREFIX}{each_missing_name}",
|
|
170
|
+
file=sys.stderr,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def main() -> int:
|
|
175
|
+
"""Entry point for the ``/hook-log-init`` slash command."""
|
|
176
|
+
all_missing_variable_names = verify_environment_variables()
|
|
177
|
+
if all_missing_variable_names:
|
|
178
|
+
_print_missing_environment_variables(all_missing_variable_names)
|
|
179
|
+
return EXIT_CODE_ENVIRONMENT_MISSING
|
|
180
|
+
neon_connection = connect_to_neon()
|
|
181
|
+
try:
|
|
182
|
+
apply_schema(neon_connection)
|
|
183
|
+
run_sentinel_round_trip(neon_connection)
|
|
184
|
+
row_count_value = _fetch_row_count(neon_connection)
|
|
185
|
+
neon_host_string = _extract_host_from_database_url(
|
|
186
|
+
os.environ[NEON_DATABASE_URL_ENVIRONMENT_VARIABLE],
|
|
187
|
+
)
|
|
188
|
+
print_success_report(
|
|
189
|
+
neon_host=neon_host_string,
|
|
190
|
+
table_name=HOOK_EVENTS_TABLE_NAME,
|
|
191
|
+
row_count=row_count_value,
|
|
192
|
+
)
|
|
193
|
+
return EXIT_CODE_SUCCESS
|
|
194
|
+
finally:
|
|
195
|
+
try:
|
|
196
|
+
neon_connection.close()
|
|
197
|
+
except Exception:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
sys.exit(main())
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Stop-hook wrapper for hook_log_extractor that never surfaces a hook failure.
|
|
3
|
+
|
|
4
|
+
Invokes ``hook_log_extractor.py --incremental`` via ``bws run`` only when
|
|
5
|
+
both ``bws`` is on PATH and ``BWS_ACCESS_TOKEN`` is set; otherwise falls
|
|
6
|
+
through to run the extractor directly. The extractor itself exits 0 when
|
|
7
|
+
``NEON_HOOK_LOGS_DATABASE_URL`` is unset, so the wrapper can rely on that
|
|
8
|
+
offline-graceful path. This wrapper always exits 0 so the Stop hook
|
|
9
|
+
never blocks session end on a missing dependency.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
if str(Path(__file__).resolve().parent.parent) not in sys.path:
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
22
|
+
|
|
23
|
+
from config.hook_log_extractor_constants import (
|
|
24
|
+
BWS_ACCESS_TOKEN_ENV_VAR,
|
|
25
|
+
BWS_EXECUTABLE_NAME,
|
|
26
|
+
BWS_RUN_SEPARATOR,
|
|
27
|
+
BWS_RUN_SUBCOMMAND,
|
|
28
|
+
EXIT_CODE_SUCCESS,
|
|
29
|
+
FLAG_INCREMENTAL,
|
|
30
|
+
STOP_WRAPPER_EXTRACTOR_SCRIPT_NAME,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _extractor_script_path() -> str:
|
|
35
|
+
return str(
|
|
36
|
+
Path(__file__).resolve().parent / STOP_WRAPPER_EXTRACTOR_SCRIPT_NAME
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _can_use_bws() -> bool:
|
|
41
|
+
if not os.environ.get(BWS_ACCESS_TOKEN_ENV_VAR):
|
|
42
|
+
return False
|
|
43
|
+
return shutil.which(BWS_EXECUTABLE_NAME) is not None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _run_with_bws() -> None:
|
|
47
|
+
subprocess.run(
|
|
48
|
+
[
|
|
49
|
+
BWS_EXECUTABLE_NAME,
|
|
50
|
+
BWS_RUN_SUBCOMMAND,
|
|
51
|
+
BWS_RUN_SEPARATOR,
|
|
52
|
+
sys.executable,
|
|
53
|
+
_extractor_script_path(),
|
|
54
|
+
FLAG_INCREMENTAL,
|
|
55
|
+
],
|
|
56
|
+
check=False,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _run_without_bws() -> None:
|
|
61
|
+
subprocess.run(
|
|
62
|
+
[
|
|
63
|
+
sys.executable,
|
|
64
|
+
_extractor_script_path(),
|
|
65
|
+
FLAG_INCREMENTAL,
|
|
66
|
+
],
|
|
67
|
+
check=False,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main() -> int:
|
|
72
|
+
"""Invoke the extractor with or without bws; swallow all failures."""
|
|
73
|
+
try:
|
|
74
|
+
if _can_use_bws():
|
|
75
|
+
_run_with_bws()
|
|
76
|
+
else:
|
|
77
|
+
_run_without_bws()
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
return EXIT_CODE_SUCCESS
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
sys.exit(main())
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Hook-Log Diagnostic Migrations
|
|
2
|
+
|
|
3
|
+
One-time DDL migrations for the hook-log diagnostic store. Each file is named
|
|
4
|
+
`YYYY-MM-DD-<short-description>.sql` and contains idempotent statements that
|
|
5
|
+
can be re-run without error.
|
|
6
|
+
|
|
7
|
+
These files are records of operations already executed (or to be executed)
|
|
8
|
+
against a specific Neon project. They are not run automatically by any hook.
|
|
9
|
+
|
|
10
|
+
## How to apply
|
|
11
|
+
|
|
12
|
+
A migration runs against the Postgres connection URL of the project being
|
|
13
|
+
modified. The runner is whichever client the operator prefers; a typical
|
|
14
|
+
pattern is:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
psql "$DATABASE_URL" -f packages/claude-dev-env/hooks/diagnostic/migrations/<file>.sql
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
When the project is hosted on Neon, the `mcp__neon__run_sql_transaction` tool
|
|
21
|
+
accepts the same statements as a list of strings. That is the path used for
|
|
22
|
+
the 2026-04-25 isolation migration described below, since the operator
|
|
23
|
+
already has Neon MCP authenticated.
|
|
24
|
+
|
|
25
|
+
## 2026-04-25-drop-themes-hook-events.sql
|
|
26
|
+
|
|
27
|
+
**Target:** Neon project `still-dust-13937951` ("Themes"). This is the
|
|
28
|
+
production themes-asset-database project, which up to PR #257 was also
|
|
29
|
+
holding the hook-log diagnostic table because the
|
|
30
|
+
`NEON_HOOK_LOGS_DATABASE_URL` Bitwarden secret was pointing at it.
|
|
31
|
+
|
|
32
|
+
**Effect:** Drops the diagnostic view `blocked_commands` and the diagnostic
|
|
33
|
+
table `hook_events` from the Themes project. The two objects were created
|
|
34
|
+
by `schema.sql` during a misrouted live test in session 80; they share no
|
|
35
|
+
foreign keys or other coupling with the themes-domain tables (`assets`,
|
|
36
|
+
`themes`, `monthly_sales`, etc.) and removing them is a pure cleanup.
|
|
37
|
+
|
|
38
|
+
**State before this migration:** `hook_events` holds 10,684 rows spanning
|
|
39
|
+
2026-04-12 through 2026-04-24. None of these rows are migrated; the
|
|
40
|
+
"start fresh" decision (recorded in PR #261) means the extractor's
|
|
41
|
+
`--full-rebuild` mode rebuilds the table contents from local JSONL on
|
|
42
|
+
its next run against the new project.
|
|
43
|
+
|
|
44
|
+
**State after this migration:** Themes contains only its production
|
|
45
|
+
domain tables. The hook-log diagnostic store lives entirely in the new
|
|
46
|
+
Neon project `winter-haze-99075918` ("claude-hook-logs"), which the
|
|
47
|
+
updated `NEON_HOOK_LOGS_DATABASE_URL` secret now points at.
|
|
48
|
+
|
|
49
|
+
**Verification:** Before applying, confirm there is no other consumer
|
|
50
|
+
of `blocked_commands` or `hook_events` in the Themes project:
|
|
51
|
+
|
|
52
|
+
```sql
|
|
53
|
+
SELECT table_name, view_definition
|
|
54
|
+
FROM information_schema.views
|
|
55
|
+
WHERE table_schema = 'public'
|
|
56
|
+
AND view_definition ILIKE '%hook_events%'
|
|
57
|
+
AND table_name <> 'blocked_commands';
|
|
58
|
+
|
|
59
|
+
SELECT conname, conrelid::regclass
|
|
60
|
+
FROM pg_constraint
|
|
61
|
+
WHERE confrelid = 'hook_events'::regclass;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The first query excludes `blocked_commands` itself, since that view (also dropped by this migration) references `hook_events` in its definition and would otherwise always match.
|
|
65
|
+
|
|
66
|
+
Both queries should return zero rows; if either returns anything,
|
|
67
|
+
investigate the dependency before running the drop.
|
|
68
|
+
|
|
69
|
+
After applying, confirm the objects are gone:
|
|
70
|
+
|
|
71
|
+
```sql
|
|
72
|
+
SELECT table_schema, table_name, table_type
|
|
73
|
+
FROM information_schema.tables
|
|
74
|
+
WHERE table_schema = 'public' AND table_name IN ('hook_events', 'blocked_commands');
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Should return zero rows.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- Recent block details for the top-blocking hook in the last 30 days.
|
|
2
|
+
-- Shows the 25 most recent blocked attempts with the command excerpt and error
|
|
3
|
+
-- excerpt so diagnosis can focus on concrete failing commands rather than
|
|
4
|
+
-- aggregate counts. To target a specific hook, filter by hook_name in a follow-up.
|
|
5
|
+
WITH top_hook AS (
|
|
6
|
+
SELECT hook_name
|
|
7
|
+
FROM hook_events
|
|
8
|
+
WHERE outcome = 'blocked'
|
|
9
|
+
AND event_timestamp >= (NOW() - INTERVAL '30 days')
|
|
10
|
+
GROUP BY hook_name
|
|
11
|
+
ORDER BY COUNT(*) DESC
|
|
12
|
+
LIMIT 1
|
|
13
|
+
)
|
|
14
|
+
SELECT
|
|
15
|
+
event_timestamp,
|
|
16
|
+
session_id,
|
|
17
|
+
hook_event,
|
|
18
|
+
hook_name,
|
|
19
|
+
tool_name,
|
|
20
|
+
command_excerpt,
|
|
21
|
+
stderr_excerpt
|
|
22
|
+
FROM hook_events
|
|
23
|
+
WHERE outcome = 'blocked'
|
|
24
|
+
AND hook_name = (SELECT hook_name FROM top_hook)
|
|
25
|
+
ORDER BY event_timestamp DESC
|
|
26
|
+
LIMIT 25;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- Total blocks grouped by hook category, all history.
|
|
2
|
+
SELECT
|
|
3
|
+
hook_category,
|
|
4
|
+
COUNT(*) AS block_count,
|
|
5
|
+
COUNT(DISTINCT hook_name) AS distinct_hook_count,
|
|
6
|
+
COUNT(DISTINCT session_id) AS distinct_session_count
|
|
7
|
+
FROM hook_events
|
|
8
|
+
WHERE outcome = 'blocked'
|
|
9
|
+
GROUP BY hook_category
|
|
10
|
+
ORDER BY block_count DESC;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Blocks grouped by tool name parsed from hook_name (e.g., Bash, Write, Edit).
|
|
2
|
+
SELECT
|
|
3
|
+
COALESCE(tool_name, '(none)') AS tool_name,
|
|
4
|
+
COUNT(*) AS block_count,
|
|
5
|
+
COUNT(DISTINCT hook_name) AS distinct_hook_count
|
|
6
|
+
FROM hook_events
|
|
7
|
+
WHERE outcome = 'blocked'
|
|
8
|
+
GROUP BY tool_name
|
|
9
|
+
ORDER BY block_count DESC;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- Daily block counts for the last 7 days, grouped by hook.
|
|
2
|
+
SELECT
|
|
3
|
+
DATE_TRUNC('day', event_timestamp)::DATE AS block_day,
|
|
4
|
+
hook_name,
|
|
5
|
+
hook_category,
|
|
6
|
+
COUNT(*) AS block_count
|
|
7
|
+
FROM hook_events
|
|
8
|
+
WHERE outcome = 'blocked'
|
|
9
|
+
AND event_timestamp >= (NOW() - INTERVAL '7 days')
|
|
10
|
+
GROUP BY block_day, hook_name, hook_category
|
|
11
|
+
ORDER BY block_day DESC, block_count DESC;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Top 10 hooks by blocking count in the last 24 hours.
|
|
2
|
+
SELECT
|
|
3
|
+
hook_name,
|
|
4
|
+
hook_category,
|
|
5
|
+
COUNT(*) AS block_count_last_24_hours,
|
|
6
|
+
MIN(COALESCE(command_excerpt, stdout_excerpt, stderr_excerpt, '')) AS top_blocked_command_preview
|
|
7
|
+
FROM hook_events
|
|
8
|
+
WHERE outcome = 'blocked'
|
|
9
|
+
AND event_timestamp >= (NOW() - INTERVAL '1 day')
|
|
10
|
+
GROUP BY hook_name, hook_category
|
|
11
|
+
ORDER BY block_count_last_24_hours DESC
|
|
12
|
+
LIMIT 10;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Top 20 hooks by blocking count across all history.
|
|
2
|
+
SELECT
|
|
3
|
+
hook_name,
|
|
4
|
+
hook_category,
|
|
5
|
+
COUNT(*) AS block_count,
|
|
6
|
+
MIN(event_timestamp) AS first_block_at,
|
|
7
|
+
MAX(event_timestamp) AS last_block_at
|
|
8
|
+
FROM hook_events
|
|
9
|
+
WHERE outcome = 'blocked'
|
|
10
|
+
GROUP BY hook_name, hook_category
|
|
11
|
+
ORDER BY block_count DESC
|
|
12
|
+
LIMIT 20;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
psycopg[binary]>=3.1,<4
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS hook_events (
|
|
2
|
+
id BIGSERIAL PRIMARY KEY,
|
|
3
|
+
event_timestamp TIMESTAMPTZ NOT NULL,
|
|
4
|
+
session_id TEXT NOT NULL,
|
|
5
|
+
cwd TEXT,
|
|
6
|
+
git_branch TEXT,
|
|
7
|
+
hook_event TEXT NOT NULL,
|
|
8
|
+
hook_name TEXT NOT NULL,
|
|
9
|
+
hook_category TEXT NOT NULL,
|
|
10
|
+
script_path TEXT,
|
|
11
|
+
tool_name TEXT,
|
|
12
|
+
tool_use_id TEXT,
|
|
13
|
+
outcome TEXT NOT NULL,
|
|
14
|
+
exit_code INTEGER,
|
|
15
|
+
duration_ms INTEGER,
|
|
16
|
+
command_excerpt TEXT,
|
|
17
|
+
stdout_excerpt TEXT,
|
|
18
|
+
stderr_excerpt TEXT,
|
|
19
|
+
source_jsonl_path TEXT NOT NULL,
|
|
20
|
+
source_line_number INTEGER NOT NULL,
|
|
21
|
+
CONSTRAINT hook_events_source_location_unique UNIQUE (source_jsonl_path, source_line_number)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_hook_events_category_outcome ON hook_events (hook_category, outcome);
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_hook_events_timestamp ON hook_events (event_timestamp DESC);
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_hook_events_hook_name ON hook_events (hook_name);
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_hook_events_session ON hook_events (session_id);
|
|
28
|
+
|
|
29
|
+
CREATE OR REPLACE VIEW blocked_commands AS
|
|
30
|
+
SELECT
|
|
31
|
+
id,
|
|
32
|
+
event_timestamp,
|
|
33
|
+
session_id,
|
|
34
|
+
cwd,
|
|
35
|
+
git_branch,
|
|
36
|
+
hook_event,
|
|
37
|
+
hook_name,
|
|
38
|
+
hook_category,
|
|
39
|
+
script_path,
|
|
40
|
+
tool_name,
|
|
41
|
+
tool_use_id,
|
|
42
|
+
outcome,
|
|
43
|
+
exit_code,
|
|
44
|
+
duration_ms,
|
|
45
|
+
command_excerpt,
|
|
46
|
+
stdout_excerpt,
|
|
47
|
+
stderr_excerpt,
|
|
48
|
+
source_jsonl_path,
|
|
49
|
+
source_line_number
|
|
50
|
+
FROM hook_events
|
|
51
|
+
WHERE outcome = 'blocked';
|