capt-hook 9.3.0__tar.gz → 9.4.0__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.
- {capt_hook-9.3.0 → capt_hook-9.4.0}/PKG-INFO +1 -1
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/app.py +3 -4
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/cli.py +12 -1
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/hooks/hooks.json +26 -13
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/loader.py +24 -2
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/once.py +2 -1
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/tombstones.py +4 -2
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/manager.py +5 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/steering/capt-hook.toml +1 -1
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/steering/steering.py +309 -9
- capt_hook-9.4.0/captain_hook/review/announce.py +127 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/review/cli.py +15 -5
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/review/dashboard.py +53 -10
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/review/fix.py +138 -37
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/review/judge.py +12 -3
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/review/pipeline.py +146 -18
- capt_hook-9.4.0/captain_hook/review/routing.py +108 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/review/scan.py +92 -12
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/review/settings.py +4 -1
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/review/store.py +616 -49
- capt_hook-9.4.0/captain_hook/review/sync.py +185 -0
- capt_hook-9.4.0/captain_hook/review/triage.py +114 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/signals/nlp.py +72 -14
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/skills/authoring-hooks/SKILL.md +8 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/skills/scanning-sessions/SKILL.md +63 -20
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +56 -3
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +47 -13
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/state.py +15 -2
- {capt_hook-9.3.0 → capt_hook-9.4.0}/pyproject.toml +1 -1
- capt_hook-9.3.0/captain_hook/review/sync.py +0 -103
- {capt_hook-9.3.0 → capt_hook-9.4.0}/LICENSE +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/README.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/.claude-plugin/plugin.json +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/__init__.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/__main__.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/ast_grep.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/classifiers/__init__.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/classifiers/conductor.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/conditions.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/context.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/contexts.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/decisions.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/dispatch.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/durable.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/events.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/fields.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/file.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/log.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/__init__.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/fixes/__init__.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/fixes/capt-hook.toml +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/fixes/teammate_permissions.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/__init__.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/_lib.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/capt-hook.toml +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/commands.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/comments.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/detours.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/docs.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/models.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/plans.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/fragments/deliverable_rubric.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/fragments/workflow_script_header.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/models/implementation_spawn_nudge.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/models/inline_edit_nudge.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/models/prose_spawn_gate.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/models/prose_workflow_nudge.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/models/review_routing_spawn_nudge.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/models/review_routing_workflow_nudge.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/models/writing_docs_spawn_nudge.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/prompts/models/writing_docs_workflow_nudge.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/prompts.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/review.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/general/tasks.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/go/__init__.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/go/capt-hook.toml +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/go/testing.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/go/toolchain.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/python/__init__.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/python/capt-hook.toml +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/python/style.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/python/testing.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/python/toolchain.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/steering/__init__.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/packs/steering/teammates.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/primitives/__init__.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/primitives/commands.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/primitives/llm.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/primitives/permissions.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/primitives/rewrite.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/primitives/workflow.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/prompt.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/py.typed +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/review/__init__.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/review/formats.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/review/prompts.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/review/repo.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/session.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/settings.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/signals/__init__.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/style/__init__.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/style/ast_grep.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/style/matchers.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/style/scope.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/style/types.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/tasks.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/testing/__init__.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/testing/helpers.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/testing/types.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/transcripts.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/types.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/util/__init__.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/util/automode.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/util/fs.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/util/http.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/util/model_cache.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/util/paths.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/util/proc.py +0 -0
- {capt_hook-9.3.0 → capt_hook-9.4.0}/captain_hook/util/text.py +0 -0
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
from typing import TYPE_CHECKING, get_args
|
|
9
9
|
|
|
10
10
|
from captain_hook.conditions import matches_conditions
|
|
11
|
+
from captain_hook.state import caller_file, hook_name
|
|
11
12
|
from captain_hook.types import (
|
|
12
13
|
Agent,
|
|
13
14
|
Command,
|
|
@@ -113,7 +114,6 @@ class State:
|
|
|
113
114
|
settings: HooksSettings | None = None
|
|
114
115
|
classifier: UserClassifier | None = None
|
|
115
116
|
load_errors: list[LoadError] = field(default_factory=list)
|
|
116
|
-
counter: int = field(default=0, repr=False)
|
|
117
117
|
|
|
118
118
|
|
|
119
119
|
_state = State()
|
|
@@ -123,7 +123,6 @@ def reset() -> None:
|
|
|
123
123
|
_state.hooks.clear()
|
|
124
124
|
_state.gitignore_patterns.clear()
|
|
125
125
|
_state.load_errors.clear()
|
|
126
|
-
_state.counter = 0
|
|
127
126
|
_state.settings = None
|
|
128
127
|
_state.classifier = None
|
|
129
128
|
|
|
@@ -161,7 +160,6 @@ def hook(
|
|
|
161
160
|
) -> None:
|
|
162
161
|
validate_conditions(only_if, "only_if", events)
|
|
163
162
|
validate_conditions(skip_if, "skip_if", events)
|
|
164
|
-
_state.counter += 1
|
|
165
163
|
_state.hooks.append(
|
|
166
164
|
RegisteredHook(
|
|
167
165
|
spec=HookSpec(
|
|
@@ -176,7 +174,8 @@ def hook(
|
|
|
176
174
|
async_=async_,
|
|
177
175
|
skip_planning_agents=skip_planning_agents,
|
|
178
176
|
),
|
|
179
|
-
name=
|
|
177
|
+
name=hook_name("hook", None, message),
|
|
178
|
+
source_file=caller_file(),
|
|
180
179
|
)
|
|
181
180
|
)
|
|
182
181
|
|
|
@@ -17,7 +17,14 @@ from loguru import logger
|
|
|
17
17
|
|
|
18
18
|
from captain_hook.app import _state, load_gitignore, reset
|
|
19
19
|
from captain_hook.dispatch import dispatch
|
|
20
|
-
from captain_hook.loader import
|
|
20
|
+
from captain_hook.loader import (
|
|
21
|
+
CONF_MODULE,
|
|
22
|
+
discover_hooks,
|
|
23
|
+
discover_pack,
|
|
24
|
+
is_skip_marked,
|
|
25
|
+
register_nlp_provisioning,
|
|
26
|
+
register_pr_announcements,
|
|
27
|
+
)
|
|
21
28
|
from captain_hook.log import setup_logging
|
|
22
29
|
from captain_hook.once import claim_once
|
|
23
30
|
from captain_hook.packs import manager
|
|
@@ -54,6 +61,10 @@ class CliState:
|
|
|
54
61
|
project_nlp = any(pack_.manifest.nlp for pack_ in resolved)
|
|
55
62
|
if project_nlp:
|
|
56
63
|
register_nlp_provisioning()
|
|
64
|
+
# The PR announcer is one shared sync SessionStart hook, registered unconditionally.
|
|
65
|
+
# All gating (spawned run, no repo, no DB, repo not watching) lives in
|
|
66
|
+
# collect_announcements, so registration needs no settings or store inspection here.
|
|
67
|
+
register_pr_announcements()
|
|
57
68
|
attached = self.attached_packs(resolved, session_dir)
|
|
58
69
|
for pack_ in attached:
|
|
59
70
|
discover_pack(pack_.entry.name, pack_.path)
|
|
@@ -3,85 +3,98 @@
|
|
|
3
3
|
"PreToolUse": [
|
|
4
4
|
{
|
|
5
5
|
"hooks": [
|
|
6
|
-
{ "type": "command", "command": "uvx capt-hook run PreToolUse" }
|
|
6
|
+
{ "type": "command", "command": "uvx capt-hook run PreToolUse" },
|
|
7
|
+
{ "type": "command", "command": "uvx capt-hook run PreToolUse --async", "async": true }
|
|
7
8
|
]
|
|
8
9
|
}
|
|
9
10
|
],
|
|
10
11
|
"PostToolUse": [
|
|
11
12
|
{
|
|
12
13
|
"hooks": [
|
|
13
|
-
{ "type": "command", "command": "uvx capt-hook run PostToolUse" }
|
|
14
|
+
{ "type": "command", "command": "uvx capt-hook run PostToolUse" },
|
|
15
|
+
{ "type": "command", "command": "uvx capt-hook run PostToolUse --async", "async": true }
|
|
14
16
|
]
|
|
15
17
|
}
|
|
16
18
|
],
|
|
17
19
|
"PostToolUseFailure": [
|
|
18
20
|
{
|
|
19
21
|
"hooks": [
|
|
20
|
-
{ "type": "command", "command": "uvx capt-hook run PostToolUseFailure" }
|
|
22
|
+
{ "type": "command", "command": "uvx capt-hook run PostToolUseFailure" },
|
|
23
|
+
{ "type": "command", "command": "uvx capt-hook run PostToolUseFailure --async", "async": true }
|
|
21
24
|
]
|
|
22
25
|
}
|
|
23
26
|
],
|
|
24
27
|
"UserPromptSubmit": [
|
|
25
28
|
{
|
|
26
29
|
"hooks": [
|
|
27
|
-
{ "type": "command", "command": "uvx capt-hook run UserPromptSubmit" }
|
|
30
|
+
{ "type": "command", "command": "uvx capt-hook run UserPromptSubmit" },
|
|
31
|
+
{ "type": "command", "command": "uvx capt-hook run UserPromptSubmit --async", "async": true }
|
|
28
32
|
]
|
|
29
33
|
}
|
|
30
34
|
],
|
|
31
35
|
"Stop": [
|
|
32
36
|
{
|
|
33
37
|
"hooks": [
|
|
34
|
-
{ "type": "command", "command": "uvx capt-hook run Stop" }
|
|
38
|
+
{ "type": "command", "command": "uvx capt-hook run Stop" },
|
|
39
|
+
{ "type": "command", "command": "uvx capt-hook run Stop --async", "async": true }
|
|
35
40
|
]
|
|
36
41
|
}
|
|
37
42
|
],
|
|
38
43
|
"SubagentStop": [
|
|
39
44
|
{
|
|
40
45
|
"hooks": [
|
|
41
|
-
{ "type": "command", "command": "uvx capt-hook run SubagentStop" }
|
|
46
|
+
{ "type": "command", "command": "uvx capt-hook run SubagentStop" },
|
|
47
|
+
{ "type": "command", "command": "uvx capt-hook run SubagentStop --async", "async": true }
|
|
42
48
|
]
|
|
43
49
|
}
|
|
44
50
|
],
|
|
45
51
|
"SubagentStart": [
|
|
46
52
|
{
|
|
47
53
|
"hooks": [
|
|
48
|
-
{ "type": "command", "command": "uvx capt-hook run SubagentStart" }
|
|
54
|
+
{ "type": "command", "command": "uvx capt-hook run SubagentStart" },
|
|
55
|
+
{ "type": "command", "command": "uvx capt-hook run SubagentStart --async", "async": true }
|
|
49
56
|
]
|
|
50
57
|
}
|
|
51
58
|
],
|
|
52
59
|
"PreCompact": [
|
|
53
60
|
{
|
|
54
61
|
"hooks": [
|
|
55
|
-
{ "type": "command", "command": "uvx capt-hook run PreCompact" }
|
|
62
|
+
{ "type": "command", "command": "uvx capt-hook run PreCompact" },
|
|
63
|
+
{ "type": "command", "command": "uvx capt-hook run PreCompact --async", "async": true }
|
|
56
64
|
]
|
|
57
65
|
}
|
|
58
66
|
],
|
|
59
67
|
"Notification": [
|
|
60
68
|
{
|
|
61
69
|
"hooks": [
|
|
62
|
-
{ "type": "command", "command": "uvx capt-hook run Notification" }
|
|
70
|
+
{ "type": "command", "command": "uvx capt-hook run Notification" },
|
|
71
|
+
{ "type": "command", "command": "uvx capt-hook run Notification --async", "async": true }
|
|
63
72
|
]
|
|
64
73
|
}
|
|
65
74
|
],
|
|
66
75
|
"PermissionRequest": [
|
|
67
76
|
{
|
|
68
77
|
"hooks": [
|
|
69
|
-
{ "type": "command", "command": "uvx capt-hook run PermissionRequest" }
|
|
78
|
+
{ "type": "command", "command": "uvx capt-hook run PermissionRequest" },
|
|
79
|
+
{ "type": "command", "command": "uvx capt-hook run PermissionRequest --async", "async": true }
|
|
70
80
|
]
|
|
71
81
|
}
|
|
72
82
|
],
|
|
73
83
|
"SessionStart": [
|
|
74
84
|
{
|
|
75
85
|
"hooks": [
|
|
76
|
-
{ "type": "command", "command": "uvx capt-hook run SessionStart
|
|
86
|
+
{ "type": "command", "command": "uvx capt-hook run SessionStart" },
|
|
87
|
+
{ "type": "command", "command": "uvx capt-hook run SessionStart --async", "async": true },
|
|
88
|
+
{ "type": "command", "command": "uvx capt-hook review run", "async": true }
|
|
77
89
|
]
|
|
78
90
|
}
|
|
79
91
|
],
|
|
80
92
|
"SessionEnd": [
|
|
81
93
|
{
|
|
82
94
|
"hooks": [
|
|
83
|
-
{ "type": "command", "command": "uvx capt-hook
|
|
84
|
-
{ "type": "command", "command": "uvx capt-hook run SessionEnd --async", "async": true }
|
|
95
|
+
{ "type": "command", "command": "uvx capt-hook run SessionEnd" },
|
|
96
|
+
{ "type": "command", "command": "uvx capt-hook run SessionEnd --async", "async": true },
|
|
97
|
+
{ "type": "command", "command": "uvx capt-hook review run", "async": true }
|
|
85
98
|
]
|
|
86
99
|
}
|
|
87
100
|
]
|
|
@@ -14,13 +14,14 @@ from typing import TYPE_CHECKING
|
|
|
14
14
|
from loguru import logger
|
|
15
15
|
|
|
16
16
|
from captain_hook.app import LoadError, _state, on
|
|
17
|
+
from captain_hook.state import PACK_PACKAGE_PREFIX
|
|
17
18
|
from captain_hook.types import Event
|
|
18
19
|
|
|
19
20
|
if TYPE_CHECKING:
|
|
20
21
|
from captain_hook.events import BaseHookEvent
|
|
22
|
+
from captain_hook.types import HookResult
|
|
21
23
|
|
|
22
24
|
CONF_MODULE = "conf"
|
|
23
|
-
PACK_PACKAGE_PREFIX = "captain_hook._packs"
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
def is_test_module(fqn: str) -> bool:
|
|
@@ -124,8 +125,29 @@ def register_nlp_provisioning() -> None:
|
|
|
124
125
|
ensure_nlp_resources()
|
|
125
126
|
|
|
126
127
|
|
|
128
|
+
def register_pr_announcements() -> None:
|
|
129
|
+
"""Register the shared sync SessionStart hook that surfaces changed PR outcomes to the user.
|
|
130
|
+
|
|
131
|
+
Called once per discovery pass, unconditionally: every gate (spawned run, no repo, no
|
|
132
|
+
database, repo not watched) lives in :func:`~captain_hook.review.announce.collect_announcements`.
|
|
133
|
+
The detached reviewer opens and resolves hook PRs on its own; this hook is how a session
|
|
134
|
+
start tells the user what happened.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
@on(Event.SessionStart, max_fires=1)
|
|
138
|
+
def announce_pr_status(evt: BaseHookEvent) -> HookResult | None:
|
|
139
|
+
from captain_hook.review.announce import collect_announcements
|
|
140
|
+
from captain_hook.types import Action, HookResult
|
|
141
|
+
|
|
142
|
+
if message := collect_announcements(evt.ctx.project_root):
|
|
143
|
+
return HookResult(action=Action.warn, message=message)
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
127
147
|
def discover_pack(name: str, pack_dir: Path) -> None:
|
|
128
|
-
|
|
148
|
+
from captain_hook.packs.manager import pack_module_name
|
|
149
|
+
|
|
150
|
+
pkg = f"{PACK_PACKAGE_PREFIX}.{pack_module_name(name)}"
|
|
129
151
|
ensure_pack_package(PACK_PACKAGE_PREFIX, [])
|
|
130
152
|
ensure_pack_package(pkg, [str(pack_dir)])
|
|
131
153
|
for path in sorted(pack_dir.glob("*.py")):
|
|
@@ -4,7 +4,8 @@ Claude Code runs the byte-identical ``uvx capt-hook run <Event>`` command once p
|
|
|
4
4
|
registering source (project settings plus each plugin), so one event fans out into
|
|
5
5
|
N sibling processes reading identical stdin within milliseconds of each other. A
|
|
6
6
|
side-effecting hook then fires N times. The guard lets the first sibling win an
|
|
7
|
-
``O_EXCL`` sentinel keyed by the event and its payload; the
|
|
7
|
+
``O_EXCL`` sentinel keyed by the event, its sync/async variant, and its payload; the
|
|
8
|
+
rest exit silently.
|
|
8
9
|
|
|
9
10
|
Best-effort only: no locking beyond the atomic create, no daemon. A TTL bounds how
|
|
10
11
|
long a claim suppresses siblings (covering uvx's multi-second startup skew) and how
|
|
@@ -81,10 +81,12 @@ def is_tombstone(text: str) -> bool:
|
|
|
81
81
|
"retire",
|
|
82
82
|
"deprecate",
|
|
83
83
|
),
|
|
84
|
-
completed
|
|
84
|
+
tense="completed",
|
|
85
85
|
subject="no_nominal",
|
|
86
86
|
),
|
|
87
|
-
Clause(
|
|
87
|
+
Clause(
|
|
88
|
+
verb=Phrase("be"), adj=Phrase("previously", "formerly", "originally", "here"), tense="completed"
|
|
89
|
+
),
|
|
88
90
|
],
|
|
89
91
|
text,
|
|
90
92
|
)
|
|
@@ -51,6 +51,11 @@ PACK_NAME_RE = re.compile(r"[a-z][a-z0-9-]*")
|
|
|
51
51
|
KEEP_COMMITS = 2
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def pack_module_name(name: str) -> str:
|
|
55
|
+
"""The import-safe module component a pack loads under (a pack name's hyphens become underscores)."""
|
|
56
|
+
return re.sub(r"\W", "_", name)
|
|
57
|
+
|
|
58
|
+
|
|
54
59
|
class PackError(Exception):
|
|
55
60
|
"""A pack source, manifest, or enabled-packs entry was invalid or unresolvable."""
|
|
56
61
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name = "steering"
|
|
2
|
-
version = "0.
|
|
2
|
+
version = "0.7.0"
|
|
3
3
|
description = "Steer the agent's judgment on what's worth fixing: prefer fundamental, first-principles fixes over band-aids, don't dismiss a real pre-existing issue, don't chase trivial type noise, don't defer the requested fix by swapping in docs or a follow-up nobody asked for, and remind a starting teammate to return tight digests instead of flooding the shared context."
|
|
4
4
|
hooks = "."
|
|
5
5
|
nlp = true
|
|
@@ -47,21 +47,31 @@ nudge(
|
|
|
47
47
|
skip_if=[TypeCheckerContext()],
|
|
48
48
|
signals=Signals(
|
|
49
49
|
[
|
|
50
|
-
Signal(
|
|
51
|
-
|
|
50
|
+
Signal(
|
|
51
|
+
pattern=(
|
|
52
|
+
r"(?i)\b(?:pre-?existing|already (?:existed|present|broken)"
|
|
53
|
+
r"|(?:existing|known|old|previous) (?:issue|bug|problem|failure)s?)\b"
|
|
54
|
+
),
|
|
55
|
+
weight=1,
|
|
56
|
+
),
|
|
57
|
+
Signal(pattern=r"(?i)(?<!no change )(?<!no changes )(?:outside|beyond) (?:the )?scope", weight=1),
|
|
52
58
|
NlpSignal(
|
|
53
59
|
clauses=[
|
|
54
60
|
Clause(noun=Phrase.expand("change"), verb=Phrase("cause", "introduce"), negated=True),
|
|
55
|
-
Clause(
|
|
61
|
+
Clause(
|
|
62
|
+
noun=Phrase("issue", "bug", "problem", "error", "failure", "violation", "warning"),
|
|
63
|
+
verb=Phrase("leave"),
|
|
64
|
+
tense="prospective",
|
|
65
|
+
),
|
|
66
|
+
Clause(
|
|
67
|
+
noun=Phrase("test"),
|
|
68
|
+
verb=Phrase("leave"),
|
|
69
|
+
adj=Phrase("broken", "failing", "flaky"),
|
|
70
|
+
tense="prospective",
|
|
71
|
+
),
|
|
56
72
|
],
|
|
57
73
|
weight=2,
|
|
58
74
|
),
|
|
59
|
-
NlpSignal(
|
|
60
|
-
clauses=[
|
|
61
|
-
Clause(noun=Phrase.expand("issue"), adj=Phrase("existing", "present", "previous")),
|
|
62
|
-
],
|
|
63
|
-
weight=1,
|
|
64
|
-
),
|
|
65
75
|
],
|
|
66
76
|
threshold=2,
|
|
67
77
|
window=15,
|
|
@@ -254,6 +264,296 @@ nudge(
|
|
|
254
264
|
}
|
|
255
265
|
]
|
|
256
266
|
): Warn(),
|
|
267
|
+
# FIX-mode regression matrix (misfires watched live 2026-07-10); message byte-identical.
|
|
268
|
+
# m1 briefing-quote: dismissal quoted into a subagent briefing arrives user-role -> origin drop
|
|
269
|
+
Input(
|
|
270
|
+
transcript=[
|
|
271
|
+
{
|
|
272
|
+
"type": "user",
|
|
273
|
+
"message": {
|
|
274
|
+
"content": [
|
|
275
|
+
{
|
|
276
|
+
"type": "text",
|
|
277
|
+
"text": (
|
|
278
|
+
"Review this transcript excerpt for whether the nudge should have fired:\n"
|
|
279
|
+
"> That's a pre-existing issue, not caused by my change. I'll leave the broken "
|
|
280
|
+
"test as is since it's beyond the scope of my change.\n"
|
|
281
|
+
"Report your assessment."
|
|
282
|
+
),
|
|
283
|
+
}
|
|
284
|
+
]
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"type": "assistant",
|
|
289
|
+
"message": {"content": [{"type": "text", "text": "Starting the review of that excerpt now."}]},
|
|
290
|
+
},
|
|
291
|
+
]
|
|
292
|
+
): Allow(),
|
|
293
|
+
# m2 TaskUpdate-description: misfire note (PROSE_TOOLS path) scores "pre-existing" once post-merge
|
|
294
|
+
Input(
|
|
295
|
+
transcript=[
|
|
296
|
+
{
|
|
297
|
+
"type": "assistant",
|
|
298
|
+
"message": {
|
|
299
|
+
"content": [
|
|
300
|
+
{
|
|
301
|
+
"type": "tool_use",
|
|
302
|
+
"id": "tu_upd",
|
|
303
|
+
"name": "TaskUpdate",
|
|
304
|
+
"input": {
|
|
305
|
+
"subject": "Investigate steering nudge misfire",
|
|
306
|
+
"description": (
|
|
307
|
+
"The pre-existing issue nudge fired again on a quoted transcript; "
|
|
308
|
+
"capturing the offending shape for the fix."
|
|
309
|
+
),
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
]
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
]
|
|
316
|
+
): Allow(),
|
|
317
|
+
# m3 salvage-assessment: salvage-vs-rerun reasoning naming the misfire scores "pre-existing" once
|
|
318
|
+
Input(
|
|
319
|
+
transcript=[
|
|
320
|
+
{
|
|
321
|
+
"type": "assistant",
|
|
322
|
+
"message": {
|
|
323
|
+
"content": [
|
|
324
|
+
{
|
|
325
|
+
"type": "text",
|
|
326
|
+
"text": (
|
|
327
|
+
"Ignoring the pre-existing issue with the hook misfires for a moment — I'm "
|
|
328
|
+
"assessing the partial work to decide whether to salvage the existing progress "
|
|
329
|
+
"or rerun from scratch."
|
|
330
|
+
),
|
|
331
|
+
}
|
|
332
|
+
]
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
]
|
|
336
|
+
): Allow(),
|
|
337
|
+
# m4 quoted-fixture: this file's own fixtures read back arrive user-role -> origin drop
|
|
338
|
+
Input(
|
|
339
|
+
transcript=[
|
|
340
|
+
{
|
|
341
|
+
"type": "assistant",
|
|
342
|
+
"message": {"content": [{"type": "text", "text": "Reading the steering pack fixtures."}]},
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"type": "user",
|
|
346
|
+
"message": {
|
|
347
|
+
"content": [
|
|
348
|
+
{
|
|
349
|
+
"type": "text",
|
|
350
|
+
"text": (
|
|
351
|
+
"steering.py fixtures:\n"
|
|
352
|
+
" \"Let's just leave the login issue for now; it's pre-existing and beyond "
|
|
353
|
+
"the scope of my change.\" -> Warn()\n"
|
|
354
|
+
" \"That's a pre-existing issue, not caused by my change.\" -> Warn()"
|
|
355
|
+
),
|
|
356
|
+
}
|
|
357
|
+
]
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
]
|
|
361
|
+
): Allow(),
|
|
362
|
+
# m5 self-echo: the nudge's own opening sentence echoed back scores "pre-existing" once post-merge
|
|
363
|
+
# (verbatim cross-turn echoes are additionally damped by echo_verbatim on a real dispatch, 8.18.0)
|
|
364
|
+
Input(
|
|
365
|
+
transcript=[
|
|
366
|
+
{
|
|
367
|
+
"type": "assistant",
|
|
368
|
+
"message": {
|
|
369
|
+
"content": [
|
|
370
|
+
{
|
|
371
|
+
"type": "text",
|
|
372
|
+
"text": (
|
|
373
|
+
"You appear to be dismissing a pre-existing issue rather than fixing it. "
|
|
374
|
+
"Acknowledged — I'll actually fix it."
|
|
375
|
+
),
|
|
376
|
+
}
|
|
377
|
+
]
|
|
378
|
+
},
|
|
379
|
+
}
|
|
380
|
+
]
|
|
381
|
+
): Allow(),
|
|
382
|
+
# m6 user-origin: full dismissal in a user message (scores 4 as assistant prose) -> origin drop
|
|
383
|
+
Input(
|
|
384
|
+
transcript=[
|
|
385
|
+
{
|
|
386
|
+
"type": "user",
|
|
387
|
+
"message": {
|
|
388
|
+
"content": [
|
|
389
|
+
{
|
|
390
|
+
"type": "text",
|
|
391
|
+
"text": (
|
|
392
|
+
"That's a pre-existing issue, not caused by my change, and it's beyond the "
|
|
393
|
+
"scope of my change — I'll leave the broken test as is."
|
|
394
|
+
),
|
|
395
|
+
}
|
|
396
|
+
]
|
|
397
|
+
},
|
|
398
|
+
}
|
|
399
|
+
]
|
|
400
|
+
): Allow(),
|
|
401
|
+
# fl1 fire-log completion: past-tense "left" (VBD) is not a prospective leave
|
|
402
|
+
Input(
|
|
403
|
+
transcript=[
|
|
404
|
+
{
|
|
405
|
+
"type": "assistant",
|
|
406
|
+
"message": {
|
|
407
|
+
"content": [
|
|
408
|
+
{
|
|
409
|
+
"type": "text",
|
|
410
|
+
"text": (
|
|
411
|
+
"Fixed the failing test and left the flaky one documented in the tracking "
|
|
412
|
+
"issue so we can revisit it deliberately."
|
|
413
|
+
),
|
|
414
|
+
}
|
|
415
|
+
]
|
|
416
|
+
},
|
|
417
|
+
}
|
|
418
|
+
]
|
|
419
|
+
): Allow(),
|
|
420
|
+
# fl2 fire-log completion: real report, "pre-existing" scored once + past "left" not matched
|
|
421
|
+
Input(
|
|
422
|
+
transcript=[
|
|
423
|
+
{
|
|
424
|
+
"type": "assistant",
|
|
425
|
+
"message": {
|
|
426
|
+
"content": [
|
|
427
|
+
{
|
|
428
|
+
"type": "text",
|
|
429
|
+
"text": (
|
|
430
|
+
"Landed the parser fix; I left the pre-existing lint warnings alone since they "
|
|
431
|
+
"predate this change, and all 42 tests pass."
|
|
432
|
+
),
|
|
433
|
+
}
|
|
434
|
+
]
|
|
435
|
+
},
|
|
436
|
+
}
|
|
437
|
+
]
|
|
438
|
+
): Allow(),
|
|
439
|
+
# tp_existing_issue: genuine dismissal pairing "pre-existing" with change-not-caused still warns
|
|
440
|
+
Input(
|
|
441
|
+
transcript=[
|
|
442
|
+
{
|
|
443
|
+
"type": "assistant",
|
|
444
|
+
"message": {
|
|
445
|
+
"content": [
|
|
446
|
+
{
|
|
447
|
+
"type": "text",
|
|
448
|
+
"text": (
|
|
449
|
+
"That's a pre-existing issue, not caused by my change — I'm not going to touch it."
|
|
450
|
+
),
|
|
451
|
+
}
|
|
452
|
+
]
|
|
453
|
+
},
|
|
454
|
+
}
|
|
455
|
+
]
|
|
456
|
+
): Warn(),
|
|
457
|
+
# tp_prospective_leave: genuine prospective dismissal ("I'll leave the broken test") still warns
|
|
458
|
+
Input(
|
|
459
|
+
transcript=[
|
|
460
|
+
{
|
|
461
|
+
"type": "assistant",
|
|
462
|
+
"message": {
|
|
463
|
+
"content": [{"type": "text", "text": "I'll leave the broken test as is."}]
|
|
464
|
+
},
|
|
465
|
+
}
|
|
466
|
+
]
|
|
467
|
+
): Warn(),
|
|
468
|
+
# tp_leave_flaky_test: a flaky (ADJ) defect qualifier on "leave the test" still warns
|
|
469
|
+
Input(
|
|
470
|
+
transcript=[
|
|
471
|
+
{
|
|
472
|
+
"type": "assistant",
|
|
473
|
+
"message": {"content": [{"type": "text", "text": "I'll leave the flaky test as is."}]},
|
|
474
|
+
}
|
|
475
|
+
]
|
|
476
|
+
): Warn(),
|
|
477
|
+
# f16 leave-test-no-defect: prospective "leave the test" with no defect qualifier is benign
|
|
478
|
+
Input(
|
|
479
|
+
transcript=[
|
|
480
|
+
{
|
|
481
|
+
"type": "assistant",
|
|
482
|
+
"message": {
|
|
483
|
+
"content": [
|
|
484
|
+
{
|
|
485
|
+
"type": "text",
|
|
486
|
+
"text": "I'll leave the integration test running while I inspect its logs.",
|
|
487
|
+
}
|
|
488
|
+
]
|
|
489
|
+
},
|
|
490
|
+
}
|
|
491
|
+
]
|
|
492
|
+
): Allow(),
|
|
493
|
+
# f17 negated-scope: "made no changes outside the scope" is scope compliance, not dismissal
|
|
494
|
+
Input(
|
|
495
|
+
transcript=[
|
|
496
|
+
{
|
|
497
|
+
"type": "assistant",
|
|
498
|
+
"message": {
|
|
499
|
+
"content": [
|
|
500
|
+
{
|
|
501
|
+
"type": "text",
|
|
502
|
+
"text": "I fixed all known issues and made no changes outside the scope of the task.",
|
|
503
|
+
}
|
|
504
|
+
]
|
|
505
|
+
},
|
|
506
|
+
}
|
|
507
|
+
]
|
|
508
|
+
): Allow(),
|
|
509
|
+
# f18a modal-perfect counterfactual: "should have left ... but I fixed it" is not a prospective leave
|
|
510
|
+
Input(
|
|
511
|
+
transcript=[
|
|
512
|
+
{
|
|
513
|
+
"type": "assistant",
|
|
514
|
+
"message": {
|
|
515
|
+
"content": [
|
|
516
|
+
{"type": "text", "text": "I should have left the broken test alone, but I fixed it."}
|
|
517
|
+
]
|
|
518
|
+
},
|
|
519
|
+
}
|
|
520
|
+
]
|
|
521
|
+
): Allow(),
|
|
522
|
+
# f18b negated prospective: "Leaving ... was never an option, so I fixed it" is not a dismissal
|
|
523
|
+
Input(
|
|
524
|
+
transcript=[
|
|
525
|
+
{
|
|
526
|
+
"type": "assistant",
|
|
527
|
+
"message": {
|
|
528
|
+
"content": [
|
|
529
|
+
{"type": "text", "text": "Leaving the test broken was never an option, so I fixed it."}
|
|
530
|
+
]
|
|
531
|
+
},
|
|
532
|
+
}
|
|
533
|
+
]
|
|
534
|
+
): Allow(),
|
|
535
|
+
# f19 previous-issue dismissal: "the previous issue is beyond the scope" warns (regex arm restored)
|
|
536
|
+
Input(
|
|
537
|
+
transcript=[
|
|
538
|
+
{
|
|
539
|
+
"type": "assistant",
|
|
540
|
+
"message": {
|
|
541
|
+
"content": [{"type": "text", "text": "The previous issue is beyond the scope of this change."}]
|
|
542
|
+
},
|
|
543
|
+
}
|
|
544
|
+
]
|
|
545
|
+
): Warn(),
|
|
546
|
+
# f21 word-boundary: "unknown bugs" must not match the "known bug" arm
|
|
547
|
+
Input(
|
|
548
|
+
transcript=[
|
|
549
|
+
{
|
|
550
|
+
"type": "assistant",
|
|
551
|
+
"message": {
|
|
552
|
+
"content": [{"type": "text", "text": "There are no unknown bugs left outside scope."}]
|
|
553
|
+
},
|
|
554
|
+
}
|
|
555
|
+
]
|
|
556
|
+
): Allow(),
|
|
257
557
|
},
|
|
258
558
|
)
|
|
259
559
|
|