capt-hook 5.0.0__tar.gz → 5.1.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-5.0.0 → capt_hook-5.1.0}/PKG-INFO +1 -1
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/classifiers/__init__.py +1 -3
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/classifiers/conductor.py +8 -8
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/cli.py +2 -2
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/loader.py +1 -3
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/general/capt-hook.toml +1 -1
- capt_hook-5.1.0/captain_hook/packs/general/models.py +133 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/general/tasks.py +1 -3
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/manager.py +15 -21
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/review/dashboard.py +1 -3
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/review/fix.py +9 -14
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/review/formats.py +4 -2
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/review/judge.py +74 -86
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/review/pipeline.py +9 -8
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/review/scan.py +4 -7
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/review/store.py +126 -130
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/settings.py +8 -8
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/style/__init__.py +2 -5
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/style/matchers.py +5 -8
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/testing/helpers.py +1 -3
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/util/http.py +1 -3
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/util/model_cache.py +11 -8
- {capt_hook-5.0.0 → capt_hook-5.1.0}/pyproject.toml +1 -1
- capt_hook-5.0.0/captain_hook/packs/general/models.py +0 -67
- {capt_hook-5.0.0 → capt_hook-5.1.0}/LICENSE +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/README.md +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/.claude-plugin/plugin.json +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/__init__.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/__main__.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/app.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/ast_grep.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/command.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/conditions.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/context.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/decisions.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/dispatch.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/durable.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/events.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/fields.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/file.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/log.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/__init__.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/general/__init__.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/general/commands.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/general/docs.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/general/plans.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/general/prompts.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/general/review.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/go/__init__.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/go/capt-hook.toml +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/go/testing.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/go/toolchain.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/python/__init__.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/python/capt-hook.toml +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/python/style.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/python/testing.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/python/toolchain.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/steering/__init__.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/steering/capt-hook.toml +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/packs/steering/steering.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/primitives/__init__.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/primitives/commands.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/primitives/llm.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/primitives/rewrite.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/primitives/workflow.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/prompt.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/py.typed +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/review/__init__.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/review/cli.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/review/repo.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/review/settings.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/review/sync.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/session.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/signals/__init__.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/signals/nlp.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/state.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/style/ast_grep.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/style/scope.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/style/types.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/tasks.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/testing/__init__.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/testing/types.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/types.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/util/__init__.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/util/fs.py +0 -0
- {capt_hook-5.0.0 → capt_hook-5.1.0}/captain_hook/util/text.py +0 -0
|
@@ -9,8 +9,6 @@ if TYPE_CHECKING:
|
|
|
9
9
|
from cc_transcript.activity import UserClassifier
|
|
10
10
|
from cc_transcript.models import TranscriptEvent
|
|
11
11
|
|
|
12
|
-
CLASSIFIER_MODULES = ("droid", "conductor", "native")
|
|
13
|
-
|
|
14
12
|
|
|
15
13
|
def detect(
|
|
16
14
|
cwd: str | None = None,
|
|
@@ -20,7 +18,7 @@ def detect(
|
|
|
20
18
|
"""Auto-detect the environment and return the user classifier for turn segmentation."""
|
|
21
19
|
return next(
|
|
22
20
|
mod.classifier
|
|
23
|
-
for name in
|
|
21
|
+
for name in ("droid", "conductor", "native")
|
|
24
22
|
if (mod := importlib.import_module(f".{name}", __package__))
|
|
25
23
|
and mod.detect(cwd=cwd, transcript_path=transcript_path, events=events)
|
|
26
24
|
)
|
|
@@ -10,16 +10,16 @@ if TYPE_CHECKING:
|
|
|
10
10
|
|
|
11
11
|
from cc_transcript.models import TranscriptEvent
|
|
12
12
|
|
|
13
|
-
SYNTHETIC_PREFIXES = (
|
|
14
|
-
"<system_instruction>",
|
|
15
|
-
"<task-notification>",
|
|
16
|
-
"<local-command-caveat>",
|
|
17
|
-
"<command-name>",
|
|
18
|
-
)
|
|
19
|
-
|
|
20
13
|
|
|
21
14
|
def classifier(event: UserEvent) -> bool:
|
|
22
|
-
return native_user_classifier(event) and not event.text.strip().startswith(
|
|
15
|
+
return native_user_classifier(event) and not event.text.strip().startswith(
|
|
16
|
+
(
|
|
17
|
+
"<system_instruction>",
|
|
18
|
+
"<task-notification>",
|
|
19
|
+
"<local-command-caveat>",
|
|
20
|
+
"<command-name>",
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def detect(
|
|
@@ -27,7 +27,6 @@ from captain_hook.types import Event
|
|
|
27
27
|
|
|
28
28
|
DIST_NAME = "capt-hook"
|
|
29
29
|
EVENT_NAMES = ", ".join(n for e in Event if (n := e.name))
|
|
30
|
-
MARKETPLACE = {"captain-hook": {"source": {"source": "github", "repo": "yasyf/captain-hook"}}}
|
|
31
30
|
PLUGIN_ID = "captain-hook@captain-hook"
|
|
32
31
|
|
|
33
32
|
|
|
@@ -79,7 +78,8 @@ def register_marketplace(root: Path) -> None:
|
|
|
79
78
|
settings_path,
|
|
80
79
|
existing
|
|
81
80
|
| {
|
|
82
|
-
"extraKnownMarketplaces": existing.get("extraKnownMarketplaces", {})
|
|
81
|
+
"extraKnownMarketplaces": existing.get("extraKnownMarketplaces", {})
|
|
82
|
+
| {"captain-hook": {"source": {"source": "github", "repo": "yasyf/captain-hook"}}},
|
|
83
83
|
"enabledPlugins": existing.get("enabledPlugins", {}) | {PLUGIN_ID: True},
|
|
84
84
|
},
|
|
85
85
|
)
|
|
@@ -17,8 +17,6 @@ from captain_hook.settings import build_settings
|
|
|
17
17
|
|
|
18
18
|
CONF_MODULE = "conf"
|
|
19
19
|
PACK_PACKAGE_PREFIX = "captain_hook._packs"
|
|
20
|
-
SKIP_MARKER = "__capt_hook_skip__"
|
|
21
|
-
SKIP_MARKER_RE = re.compile(rf"^{SKIP_MARKER}\s*=\s*True\b", re.MULTILINE)
|
|
22
20
|
|
|
23
21
|
|
|
24
22
|
def is_test_module(fqn: str) -> bool:
|
|
@@ -28,7 +26,7 @@ def is_test_module(fqn: str) -> bool:
|
|
|
28
26
|
|
|
29
27
|
def is_skip_marked(path: Path) -> bool:
|
|
30
28
|
"""True when the module declares ``__capt_hook_skip__ = True`` — a library, not an auto-loaded hook."""
|
|
31
|
-
return path.is_file() and bool(
|
|
29
|
+
return path.is_file() and bool(re.search(r"^__capt_hook_skip__\s*=\s*True\b", path.read_text(), re.MULTILINE))
|
|
32
30
|
|
|
33
31
|
|
|
34
32
|
def import_or_reload(fqn: str, fresh_this_pass: set[str]) -> ModuleType:
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from captain_hook import (
|
|
4
|
+
Agent,
|
|
5
|
+
Allow,
|
|
6
|
+
Block,
|
|
7
|
+
Event,
|
|
8
|
+
Input,
|
|
9
|
+
Rewrite,
|
|
10
|
+
Tool,
|
|
11
|
+
ToolInput,
|
|
12
|
+
Warn,
|
|
13
|
+
WorkflowScript,
|
|
14
|
+
hook,
|
|
15
|
+
nudge,
|
|
16
|
+
set_tool_input,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
hook(
|
|
20
|
+
Event.PreToolUse,
|
|
21
|
+
only_if=[Tool("Agent|Task"), ToolInput("model", r"(?i)\bhaiku\b")],
|
|
22
|
+
skip_if=[
|
|
23
|
+
ToolInput(
|
|
24
|
+
"prompt",
|
|
25
|
+
r"(?i)\b(classif|label|tag|categoriz|per.file|one (fact|thing)|mechanical|probe|ping"
|
|
26
|
+
r"|echo|smoke|count|capacit|extract)",
|
|
27
|
+
)
|
|
28
|
+
],
|
|
29
|
+
message=(
|
|
30
|
+
"This subagent is pinned to haiku. Route a subagent to haiku only for a single-fact "
|
|
31
|
+
"mechanical step — classifying, labeling, tagging, counting, or probing one thing per "
|
|
32
|
+
"item. Anything that carries judgment should run on sonnet; use model='sonnet', or drop "
|
|
33
|
+
"model entirely to inherit the session model. If this genuinely is a mechanical "
|
|
34
|
+
"single-fact step, say 'mechanical' in the prompt and retry — the haiku pin will be "
|
|
35
|
+
"allowed. See CLAUDE.md § Plan Execution & Orchestration (Models)."
|
|
36
|
+
),
|
|
37
|
+
block=True,
|
|
38
|
+
tests={
|
|
39
|
+
Input(model="haiku", prompt="implement the retry backoff in the client"): Block(),
|
|
40
|
+
Input(model="haiku", prompt="classify each file's language"): Allow(),
|
|
41
|
+
Input(model="haiku", prompt="Probe subagent capacity: spawn and return the word ok"): Allow(),
|
|
42
|
+
Input(model="haiku", prompt="mechanical step: return the repo's default branch name"): Allow(),
|
|
43
|
+
Input(model="haiku", prompt="count the TODO markers in src/"): Allow(),
|
|
44
|
+
Input(prompt="implement the retry backoff in the client"): Allow(),
|
|
45
|
+
Input(model="sonnet", prompt="implement the retry backoff in the client"): Allow(),
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
hook(
|
|
50
|
+
Event.PreToolUse,
|
|
51
|
+
only_if=[
|
|
52
|
+
Tool("Agent|Task"),
|
|
53
|
+
ToolInput("model", r"(?i)\b(haiku|sonnet|opus)\b"),
|
|
54
|
+
ToolInput("prompt", r"(?i)\b(writ(e|es|ing|ten)|draft|redraft|rewrit|revis|polish|copyedit|compose|author|update|edit)"),
|
|
55
|
+
ToolInput(
|
|
56
|
+
"prompt",
|
|
57
|
+
r"(?i)\b(readme|docs?|documentation|blog|changelog|release notes|announcement"
|
|
58
|
+
r"|tutorial|guide|prose|copywrit|marketing copy|article|newsletter|email"
|
|
59
|
+
r"|pr description|commit message)\b",
|
|
60
|
+
),
|
|
61
|
+
],
|
|
62
|
+
skip_if=[ToolInput("prompt", r"(?i)\b(classif|label|tag|categoriz|count|extract|mechanical)")],
|
|
63
|
+
message=(
|
|
64
|
+
"This subagent is pinned to a non-fable model but its prompt is prose/writing work. "
|
|
65
|
+
"All writing — docs, READMEs, release notes, any user-facing text — routes to fable: "
|
|
66
|
+
"drop model to inherit the session model, or pass model='fable'. "
|
|
67
|
+
"See CLAUDE.md § Plan Execution & Orchestration (Models)."
|
|
68
|
+
),
|
|
69
|
+
block=True,
|
|
70
|
+
tests={
|
|
71
|
+
Input(model="sonnet", prompt="Write the README quickstart for this repo"): Block(),
|
|
72
|
+
Input(model="opus", prompt="draft the release notes for v2"): Block(),
|
|
73
|
+
Input(model="haiku", prompt="update the CHANGELOG entry for the fix"): Block(),
|
|
74
|
+
Input(model="fable", prompt="write the README quickstart"): Allow(),
|
|
75
|
+
Input(prompt="write the README quickstart"): Allow(),
|
|
76
|
+
Input(model="sonnet", prompt="review the README for factual errors"): Allow(),
|
|
77
|
+
Input(model="sonnet", prompt="update the retry backoff config"): Allow(),
|
|
78
|
+
Input(model="haiku", prompt="label each README section with its Diataxis mode"): Allow(),
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
set_tool_input(
|
|
83
|
+
"model",
|
|
84
|
+
"sonnet",
|
|
85
|
+
tool="Agent|Task",
|
|
86
|
+
only_if=[Agent("Explore|claude-code-guide")],
|
|
87
|
+
note=(
|
|
88
|
+
"Upgraded this recon subagent from the silent haiku default to sonnet, per the Models "
|
|
89
|
+
"table in CLAUDE.md § Plan Execution & Orchestration (Models)."
|
|
90
|
+
),
|
|
91
|
+
tests={
|
|
92
|
+
Input(agent_type="Explore"): Rewrite(model="sonnet"),
|
|
93
|
+
Input(agent_type="Explore", model="haiku"): Allow(),
|
|
94
|
+
Input(agent_type="general-purpose"): Allow(),
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
nudge(
|
|
99
|
+
"""
|
|
100
|
+
This workflow script pins agent() steps to haiku. Reserve haiku for mechanical single-fact
|
|
101
|
+
map steps; a judgment-bearing stage should inherit the session model or route up. See
|
|
102
|
+
CLAUDE.md § Plan Execution & Orchestration (Models).
|
|
103
|
+
""",
|
|
104
|
+
only_if=[Tool("Workflow"), WorkflowScript(model="haiku")],
|
|
105
|
+
events=Event.PreToolUse,
|
|
106
|
+
max_fires=2,
|
|
107
|
+
tests={
|
|
108
|
+
Input(script="steps:\n - agent: reviewer\n model: 'haiku'\n"): Warn(),
|
|
109
|
+
Input(script="steps:\n - agent: reviewer\n model: 'sonnet'\n"): Allow(),
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
nudge(
|
|
114
|
+
"""
|
|
115
|
+
This workflow script pins a non-fable model on what looks like prose/writing stages. All
|
|
116
|
+
writing — docs, READMEs, release notes, any user-facing text — routes to fable: inherit the
|
|
117
|
+
session model or pin model: 'fable'. See CLAUDE.md § Plan Execution & Orchestration (Models).
|
|
118
|
+
""",
|
|
119
|
+
only_if=[
|
|
120
|
+
Tool("Workflow"),
|
|
121
|
+
WorkflowScript(
|
|
122
|
+
pattern=r"(?i)\b(readme|docs?|documentation|blog|changelog|release notes|prose)\b",
|
|
123
|
+
model="haiku|sonnet|opus",
|
|
124
|
+
),
|
|
125
|
+
],
|
|
126
|
+
events=Event.PreToolUse,
|
|
127
|
+
max_fires=2,
|
|
128
|
+
tests={
|
|
129
|
+
Input(script="steps:\n - agent: write the README intro\n model: 'sonnet'\n"): Warn(),
|
|
130
|
+
Input(script="steps:\n - agent: write the README intro\n model: 'fable'\n"): Allow(),
|
|
131
|
+
Input(script="steps:\n - agent: fix the retry backoff\n model: 'sonnet'\n"): Allow(),
|
|
132
|
+
},
|
|
133
|
+
)
|
|
@@ -22,8 +22,6 @@ from captain_hook import (
|
|
|
22
22
|
OVERRIDE_TOKEN = "REMAINING_TASKS_ACKNOWLEDGED"
|
|
23
23
|
TASK_DRIFT_THRESHOLD = 8
|
|
24
24
|
|
|
25
|
-
EXPLORATION_TOOLS = "Bash|Grep|Glob|WebSearch|WebFetch|LSP|Skill"
|
|
26
|
-
|
|
27
25
|
IMPERATIVES = (
|
|
28
26
|
r"\b(?:add|fix|update|change|remove|create|implement|refactor|"
|
|
29
27
|
r"move|rename|delete|replace|extract|split|merge|convert|migrate)\b"
|
|
@@ -54,7 +52,7 @@ class DriftedFromTasks(CustomCondition):
|
|
|
54
52
|
if not evt.tasks.open:
|
|
55
53
|
return False
|
|
56
54
|
since = evt.ctx.t.after(tool="TaskCreate|TaskUpdate|TaskList|TaskGet")
|
|
57
|
-
return since.tool_calls.named(
|
|
55
|
+
return since.tool_calls.named("Bash|Grep|Glob|WebSearch|WebFetch|LSP|Skill").count() >= TASK_DRIFT_THRESHOLD
|
|
58
56
|
|
|
59
57
|
|
|
60
58
|
gate(
|
|
@@ -35,23 +35,13 @@ from filelock import FileLock
|
|
|
35
35
|
from captain_hook import state
|
|
36
36
|
from captain_hook.util import http
|
|
37
37
|
|
|
38
|
-
PACKS_TOML = "packs.toml"
|
|
39
38
|
PACK_MANIFEST = "capt-hook.toml"
|
|
40
|
-
# A pack's manifest may sit in .claude/ (preferred) or at the repo root.
|
|
41
|
-
MANIFEST_MEMBERS = (f".claude/{PACK_MANIFEST}", PACK_MANIFEST)
|
|
42
39
|
SHA_MARKER = ".sha"
|
|
43
|
-
META_SUFFIX = ".meta"
|
|
44
|
-
FASTPATH_NAME = ".resolve-fastpath"
|
|
45
40
|
LATEST_REF = "latest"
|
|
46
41
|
# Moving refs (@latest / a branch / a bare default-branch source) re-resolve to a fresh
|
|
47
42
|
# commit at most once per this window; within it the cached commit is used with no network.
|
|
48
43
|
REFRESH_TTL_SECONDS = 24 * 60 * 60
|
|
49
|
-
SOURCE_RE = re.compile(r"^github:(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+?)(?:@(?P<ref>[\w./-]+))?$")
|
|
50
44
|
PACK_NAME_RE = re.compile(r"[a-z][a-z0-9-]*")
|
|
51
|
-
GITHUB_REPO = "https://api.github.com/repos/{owner}/{repo}"
|
|
52
|
-
GITHUB_COMMIT = "https://api.github.com/repos/{owner}/{repo}/commits/{ref}"
|
|
53
|
-
GITHUB_LATEST_RELEASE = "https://api.github.com/repos/{owner}/{repo}/releases/latest"
|
|
54
|
-
GITHUB_TARBALL = "https://github.com/{owner}/{repo}/archive/{sha}.tar.gz"
|
|
55
45
|
|
|
56
46
|
|
|
57
47
|
class PackError(Exception):
|
|
@@ -66,7 +56,7 @@ class PackSource:
|
|
|
66
56
|
|
|
67
57
|
@classmethod
|
|
68
58
|
def parse(cls, raw: str) -> PackSource:
|
|
69
|
-
if not (m :=
|
|
59
|
+
if not (m := re.match(r"^github:(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+?)(?:@(?P<ref>[\w./-]+))?$", raw)):
|
|
70
60
|
raise PackError(f"invalid pack source {raw!r}; expected github:owner/repo[@ref]")
|
|
71
61
|
return cls(owner=m["owner"], repo=m["repo"], ref=m["ref"])
|
|
72
62
|
|
|
@@ -147,7 +137,7 @@ class PackMeta:
|
|
|
147
137
|
|
|
148
138
|
|
|
149
139
|
def packs_toml_path(root: Path) -> Path:
|
|
150
|
-
return root / ".claude" / "hooks" /
|
|
140
|
+
return root / ".claude" / "hooks" / "packs.toml"
|
|
151
141
|
|
|
152
142
|
|
|
153
143
|
def manifest_in(root: Path) -> Path:
|
|
@@ -215,24 +205,25 @@ def packs_cache_root() -> Path:
|
|
|
215
205
|
|
|
216
206
|
|
|
217
207
|
def meta_path(name: str) -> Path:
|
|
218
|
-
return packs_cache_root() / f"{name}
|
|
208
|
+
return packs_cache_root() / f"{name}.meta"
|
|
219
209
|
|
|
220
210
|
|
|
221
211
|
def resolve_ref(source: PackSource) -> str:
|
|
222
212
|
"""Resolve a source's effective git ref: @latest via the latest release, else the ref or default branch."""
|
|
223
213
|
match source.ref:
|
|
224
214
|
case None:
|
|
225
|
-
|
|
215
|
+
url = f"https://api.github.com/repos/{source.owner}/{source.repo}"
|
|
216
|
+
return http.github_get_json(url)["default_branch"]
|
|
226
217
|
case ref if ref == LATEST_REF:
|
|
227
|
-
|
|
218
|
+
url = f"https://api.github.com/repos/{source.owner}/{source.repo}/releases/latest"
|
|
219
|
+
return http.github_get_json(url)["tag_name"]
|
|
228
220
|
case ref:
|
|
229
221
|
return ref
|
|
230
222
|
|
|
231
223
|
|
|
232
224
|
def resolve_commit(source: PackSource) -> str:
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
]
|
|
225
|
+
url = f"https://api.github.com/repos/{source.owner}/{source.repo}/commits/{resolve_ref(source)}"
|
|
226
|
+
return http.github_get_json(url)["sha"]
|
|
236
227
|
|
|
237
228
|
|
|
238
229
|
def strip_top_level(tf: tarfile.TarFile) -> Iterator[tarfile.TarInfo]:
|
|
@@ -266,14 +257,17 @@ def fetch_commit(source: PackSource, sha: str) -> ResolvedPack:
|
|
|
266
257
|
root.mkdir(parents=True, exist_ok=True)
|
|
267
258
|
with FileLock(str(root / f"{sha}.lock")):
|
|
268
259
|
tarball = root / f".tarball-{sha}.tar.gz"
|
|
269
|
-
|
|
260
|
+
url = f"https://github.com/{source.owner}/{source.repo}/archive/{sha}.tar.gz"
|
|
261
|
+
http.github_download(url, tarball)
|
|
270
262
|
staging = root / f".staging-{sha}"
|
|
271
263
|
if staging.exists():
|
|
272
264
|
shutil.rmtree(staging)
|
|
273
265
|
with tarfile.open(tarball) as tf:
|
|
274
266
|
members = list(strip_top_level(tf))
|
|
275
267
|
by_path = {m.path: m for m in members}
|
|
276
|
-
manifest_member = next(
|
|
268
|
+
manifest_member = next(
|
|
269
|
+
(by_path[p] for p in (f".claude/{PACK_MANIFEST}", PACK_MANIFEST) if p in by_path), None
|
|
270
|
+
)
|
|
277
271
|
if manifest_member is None:
|
|
278
272
|
raise PackError(f"pack manifest {PACK_MANIFEST} missing in {source}")
|
|
279
273
|
tf.extract(manifest_member, staging, filter="data")
|
|
@@ -414,7 +408,7 @@ def all_cached_and_fresh(entries: Sequence[PackEntry], now: float) -> bool:
|
|
|
414
408
|
|
|
415
409
|
|
|
416
410
|
def fastpath_path(root: Path) -> Path:
|
|
417
|
-
return packs_cache_root() / f"{sha256(str(root.resolve()).encode()).hexdigest()[:16]}
|
|
411
|
+
return packs_cache_root() / f"{sha256(str(root.resolve()).encode()).hexdigest()[:16]}.resolve-fastpath"
|
|
418
412
|
|
|
419
413
|
|
|
420
414
|
def fastpath_unchanged(root: Path, entries: Sequence[PackEntry], now: float) -> bool:
|
|
@@ -31,8 +31,6 @@ if TYPE_CHECKING:
|
|
|
31
31
|
from captain_hook.review.settings import ReviewSettings
|
|
32
32
|
from captain_hook.review.store import CandidateView, SpawnHealth
|
|
33
33
|
|
|
34
|
-
BAR_FILLED = "█"
|
|
35
|
-
BAR_EMPTY = "░"
|
|
36
34
|
DETAIL_WIDTH = 80
|
|
37
35
|
KIND_STYLE = {CandidateKind.CREATE: "cyan", CandidateKind.FIX: "magenta"}
|
|
38
36
|
SPAWN_STALE_AFTER = timedelta(days=7)
|
|
@@ -81,7 +79,7 @@ def trim(text: str, *, width: int = DETAIL_WIDTH) -> str:
|
|
|
81
79
|
def bar(done: int, need: int, *, width: int = 5) -> str:
|
|
82
80
|
cells = min(need, width)
|
|
83
81
|
filled = min(cells, round(done / need * cells)) if need else cells
|
|
84
|
-
return
|
|
82
|
+
return "█" * filled + "░" * (cells - filled)
|
|
85
83
|
|
|
86
84
|
|
|
87
85
|
def targets(view: CandidateView, settings: ReviewSettings) -> tuple[tuple[str, int, int], ...]:
|
|
@@ -62,12 +62,8 @@ HOOK_COMPLAINT = SourceKind("hook_complaint")
|
|
|
62
62
|
PROXIMITY_TURNS = 3
|
|
63
63
|
TIGHT_PROXIMITY_TURNS = 1
|
|
64
64
|
STOP_FEEDBACK_PREFIX = "Stop hook feedback:\n"
|
|
65
|
-
PRIMITIVES_DIR = "captain_hook/primitives/"
|
|
66
|
-
HOOKS_DIR = ".claude/hooks"
|
|
67
65
|
PACKS_DIR = "captain_hook/packs"
|
|
68
66
|
|
|
69
|
-
HOOK_VOCAB_RE = re.compile(r"\b(?:hook|reminder|nudge|gate|guard)s?\b", re.IGNORECASE)
|
|
70
|
-
|
|
71
67
|
STRONG_MARKERS: tuple[tuple[str, re.Pattern[str]], ...] = tuple(
|
|
72
68
|
(misfire_class, re.compile(pattern, re.IGNORECASE))
|
|
73
69
|
for misfire_class, pattern in (
|
|
@@ -82,12 +78,6 @@ STRONG_MARKERS: tuple[tuple[str, re.Pattern[str]], ...] = tuple(
|
|
|
82
78
|
)
|
|
83
79
|
)
|
|
84
80
|
|
|
85
|
-
HEDGED_MARKER_RE = re.compile(
|
|
86
|
-
r"(?:\bi think\b|\bseems?\b|\bmay\b|\bmight\b|\blooks like\b)"
|
|
87
|
-
r"[\s\S]{0,80}?(?:false[ -]positive|misfir|re-?fir|shouldn'?t have fired|spurious|unnecessar|wrong(?:ly)?)",
|
|
88
|
-
re.IGNORECASE,
|
|
89
|
-
)
|
|
90
|
-
|
|
91
81
|
COMPLIANCE_RE = re.compile(
|
|
92
82
|
r"(?:\bI'?ll\b|\blet me\b|\bgoing to\b|\bgood (?:point|catch)\b|\bnoted\b)"
|
|
93
83
|
r"[\s\S]{0,40}?(?:hook|reminder|gate|nudge)",
|
|
@@ -127,9 +117,14 @@ class Fingerprint:
|
|
|
127
117
|
|
|
128
118
|
|
|
129
119
|
def classify_marker(text: str) -> Marker | None:
|
|
130
|
-
if not
|
|
120
|
+
if not re.search(r"\b(?:hook|reminder|nudge|gate|guard)s?\b", text, re.IGNORECASE):
|
|
131
121
|
return None
|
|
132
|
-
hedged =
|
|
122
|
+
hedged = re.search(
|
|
123
|
+
r"(?:\bi think\b|\bseems?\b|\bmay\b|\bmight\b|\blooks like\b)"
|
|
124
|
+
r"[\s\S]{0,80}?(?:false[ -]positive|misfir|re-?fir|shouldn'?t have fired|spurious|unnecessar|wrong(?:ly)?)",
|
|
125
|
+
text,
|
|
126
|
+
re.IGNORECASE,
|
|
127
|
+
)
|
|
133
128
|
strong = next(((cls, m) for cls, rx in STRONG_MARKERS if (m := rx.search(text))), None)
|
|
134
129
|
if strong is not None and hedged is None:
|
|
135
130
|
return Marker("strong", strong[0], strong[1].group(0))
|
|
@@ -207,7 +202,7 @@ def attribute_fingerprint(
|
|
|
207
202
|
def resolve_target(decision: Decision) -> tuple[str, str] | None:
|
|
208
203
|
from captain_hook.packs.manager import builtin_packs
|
|
209
204
|
|
|
210
|
-
if
|
|
205
|
+
if "captain_hook/primitives/" not in decision.source_file:
|
|
211
206
|
return decision.source_file, decision.kind
|
|
212
207
|
module, sep, _ = decision.kind.partition(":")
|
|
213
208
|
if not sep or not module:
|
|
@@ -216,7 +211,7 @@ def resolve_target(decision: Decision) -> tuple[str, str] | None:
|
|
|
216
211
|
case [pack, mod] if (pack_dir := builtin_packs().get(pack)) and (pack_dir / f"{mod}.py").is_file():
|
|
217
212
|
return f"{PACKS_DIR}/{pack}/{mod}.py", decision.kind
|
|
218
213
|
case parts if all(part.isidentifier() for part in parts):
|
|
219
|
-
return f"
|
|
214
|
+
return f".claude/hooks/{parts[-1]}.py", decision.kind
|
|
220
215
|
case _:
|
|
221
216
|
return None
|
|
222
217
|
|
|
@@ -27,7 +27,6 @@ CONDUCTOR_WORKSTREAM_HEADER_RE = re.compile(
|
|
|
27
27
|
r"^### (?P<id>[A-Z][\w-]*\d*)\s*\[(?P<kind>[A-Z]+)\]\s*—\s*(?P<title>.+)$",
|
|
28
28
|
re.MULTILINE,
|
|
29
29
|
)
|
|
30
|
-
WORKSTREAM_BODY_RE = re.compile(r"^(?:FIX|Tests): .+$", re.MULTILINE)
|
|
31
30
|
CONDUCTOR_FINDING_FMT = RegexReviewFormat(
|
|
32
31
|
name="conductor-finding",
|
|
33
32
|
groups=(
|
|
@@ -70,7 +69,10 @@ def extract_conductor_workstream(text: str) -> tuple[ReviewComment, ...]:
|
|
|
70
69
|
line_end=None,
|
|
71
70
|
comment=" ".join(
|
|
72
71
|
[f"{header.group('id')} [{header.group('kind')}] {header.group('title').strip()}"]
|
|
73
|
-
+ [
|
|
72
|
+
+ [
|
|
73
|
+
line.group(0).strip()
|
|
74
|
+
for line in re.finditer(r"^(?:FIX|Tests): .+$", text[header.end() : end], re.MULTILINE)
|
|
75
|
+
]
|
|
74
76
|
),
|
|
75
77
|
)
|
|
76
78
|
for header, end in zip(
|
|
@@ -47,7 +47,6 @@ TRIGGER_BUDGET = Budget(turn_chars=2000, tool_chars=6000)
|
|
|
47
47
|
CONTEXT_BUDGET = Budget()
|
|
48
48
|
|
|
49
49
|
DURABLE_CATEGORIES = frozenset({"durable_style_rule", "workflow_rule", "tooling_rule", "safety_guard"})
|
|
50
|
-
ACCEPTED_CATEGORIES = DURABLE_CATEGORIES | {"misfire_confirmed"}
|
|
51
50
|
|
|
52
51
|
Category = Literal[
|
|
53
52
|
"durable_style_rule",
|
|
@@ -63,81 +62,6 @@ Category = Literal[
|
|
|
63
62
|
"ambient_mention",
|
|
64
63
|
]
|
|
65
64
|
|
|
66
|
-
JUDGE_PROMPT = """\
|
|
67
|
-
You are auditing one piece of feedback a developer gave an AI coding assistant
|
|
68
|
-
(Claude), deciding whether it is a DURABLE correction worth encoding as an
|
|
69
|
-
automated hook — a rule that should fire in every future session of this
|
|
70
|
-
repository — or feedback that only mattered in the moment.
|
|
71
|
-
|
|
72
|
-
Pick exactly one category:
|
|
73
|
-
- durable_style_rule: a standing code-style or API-design rule ("never use a
|
|
74
|
-
bare except", "always frozen dataclasses") that future code must follow.
|
|
75
|
-
- workflow_rule: a standing rule about process — how to plan, commit, test,
|
|
76
|
-
review, or communicate ("always run the tests before claiming done").
|
|
77
|
-
- tooling_rule: a standing rule about which tool or command to use ("use uv,
|
|
78
|
-
not pip", "search with rg, not grep").
|
|
79
|
-
- safety_guard: a standing guard against a dangerous action ("never force-push
|
|
80
|
-
to main", "never edit generated files").
|
|
81
|
-
- one_off_correction: fixes the assistant's current output without stating a
|
|
82
|
-
reusable rule ("rename this one", "the test you broke is test_foo").
|
|
83
|
-
- task_specific: a rule scoped to the current task or file, not the repository
|
|
84
|
-
("for this migration keep both columns").
|
|
85
|
-
- preference_unclear: corrective in tone, but the underlying rule cannot be
|
|
86
|
-
stated precisely enough to automate.
|
|
87
|
-
- ambient_noise: not corrective at all — status updates, questions, new tasks.
|
|
88
|
-
|
|
89
|
-
The first four categories are durable; the rest are not. A durable correction
|
|
90
|
-
states (or clearly implies) a rule that would be violated again and could be
|
|
91
|
-
checked mechanically. Words like "always", "never", or "stop doing X" are
|
|
92
|
-
strong durability signals; a rule that names one specific line, variable, or
|
|
93
|
-
test is task-scoped, not durable.
|
|
94
|
-
|
|
95
|
-
summary: ONE neutral sentence naming the rule the feedback implies (or what the
|
|
96
|
-
user reacted to when there is no rule). Write it for every category.
|
|
97
|
-
confidence: your probability (0 to 1) that your durable-vs-not call is correct.
|
|
98
|
-
rationale: one short clause.
|
|
99
|
-
|
|
100
|
-
Respond with strict JSON matching the schema — no extra keys, no prose.
|
|
101
|
-
|
|
102
|
-
[source: {source_kind}]
|
|
103
|
-
{context}
|
|
104
|
-
=== FEEDBACK TO CLASSIFY ===
|
|
105
|
-
{text}"""
|
|
106
|
-
|
|
107
|
-
FIX_JUDGE_PROMPT = """\
|
|
108
|
-
You are auditing one remark an AI coding assistant (Claude) made about an
|
|
109
|
-
automated hook that fired during its session, deciding whether the remark
|
|
110
|
-
REPORTS A MISFIRE — the hook firing wrongly or redundantly — or something else.
|
|
111
|
-
|
|
112
|
-
Pick exactly one category:
|
|
113
|
-
- misfire_confirmed: the remark asserts the hook fired wrongly — it re-fired on
|
|
114
|
-
content already addressed, flagged a false positive, or fired outside its
|
|
115
|
-
intended scope — and the surrounding conversation is consistent with that
|
|
116
|
-
claim.
|
|
117
|
-
- compliance: the remark acknowledges the hook's message and follows it (or
|
|
118
|
-
promises to follow it going forward).
|
|
119
|
-
- ambient_mention: the hook is merely described, quoted, or referenced in
|
|
120
|
-
passing, with no claim that it fired wrongly.
|
|
121
|
-
|
|
122
|
-
Only misfire_confirmed marks the hook as worth amending. A remark that both
|
|
123
|
-
complies and dismisses ("noted, but this re-fired on text I already fixed") is
|
|
124
|
-
misfire_confirmed — the dismissal is the signal. A remark that merely reports
|
|
125
|
-
the hook fired, or works around it without disputing it, is not.
|
|
126
|
-
|
|
127
|
-
summary: ONE neutral sentence naming what the hook did and what Claude claims
|
|
128
|
-
about it. Write it for every category.
|
|
129
|
-
confidence: your probability (0 to 1) that your misfire-vs-not call is correct.
|
|
130
|
-
rationale: one short clause.
|
|
131
|
-
|
|
132
|
-
Respond with strict JSON matching the schema — no extra keys, no prose.
|
|
133
|
-
|
|
134
|
-
[hook: {hook_name} ({event}/{action})]
|
|
135
|
-
=== the hook's fire message ===
|
|
136
|
-
{fire_message}
|
|
137
|
-
{context}
|
|
138
|
-
=== REMARK TO CLASSIFY ===
|
|
139
|
-
{text}"""
|
|
140
|
-
|
|
141
65
|
|
|
142
66
|
class ReviewVerdict(BaseModel):
|
|
143
67
|
"""One judge verdict on a stored correction.
|
|
@@ -157,7 +81,7 @@ class ReviewVerdict(BaseModel):
|
|
|
157
81
|
@property
|
|
158
82
|
def accepted(self) -> bool:
|
|
159
83
|
"""Whether the category marks a durable correction or a confirmed misfire."""
|
|
160
|
-
return self.category in
|
|
84
|
+
return self.category in DURABLE_CATEGORIES | {"misfire_confirmed"}
|
|
161
85
|
|
|
162
86
|
|
|
163
87
|
@dataclass(frozen=True, slots=True)
|
|
@@ -208,19 +132,83 @@ async def render_context(window: ContextWindow) -> tuple[str, Fidelity]:
|
|
|
208
132
|
|
|
209
133
|
|
|
210
134
|
def build_create_prompt(row: Mapping[str, object], context: str) -> str:
|
|
211
|
-
return
|
|
135
|
+
return f"""\
|
|
136
|
+
You are auditing one piece of feedback a developer gave an AI coding assistant
|
|
137
|
+
(Claude), deciding whether it is a DURABLE correction worth encoding as an
|
|
138
|
+
automated hook — a rule that should fire in every future session of this
|
|
139
|
+
repository — or feedback that only mattered in the moment.
|
|
140
|
+
|
|
141
|
+
Pick exactly one category:
|
|
142
|
+
- durable_style_rule: a standing code-style or API-design rule ("never use a
|
|
143
|
+
bare except", "always frozen dataclasses") that future code must follow.
|
|
144
|
+
- workflow_rule: a standing rule about process — how to plan, commit, test,
|
|
145
|
+
review, or communicate ("always run the tests before claiming done").
|
|
146
|
+
- tooling_rule: a standing rule about which tool or command to use ("use uv,
|
|
147
|
+
not pip", "search with rg, not grep").
|
|
148
|
+
- safety_guard: a standing guard against a dangerous action ("never force-push
|
|
149
|
+
to main", "never edit generated files").
|
|
150
|
+
- one_off_correction: fixes the assistant's current output without stating a
|
|
151
|
+
reusable rule ("rename this one", "the test you broke is test_foo").
|
|
152
|
+
- task_specific: a rule scoped to the current task or file, not the repository
|
|
153
|
+
("for this migration keep both columns").
|
|
154
|
+
- preference_unclear: corrective in tone, but the underlying rule cannot be
|
|
155
|
+
stated precisely enough to automate.
|
|
156
|
+
- ambient_noise: not corrective at all — status updates, questions, new tasks.
|
|
157
|
+
|
|
158
|
+
The first four categories are durable; the rest are not. A durable correction
|
|
159
|
+
states (or clearly implies) a rule that would be violated again and could be
|
|
160
|
+
checked mechanically. Words like "always", "never", or "stop doing X" are
|
|
161
|
+
strong durability signals; a rule that names one specific line, variable, or
|
|
162
|
+
test is task-scoped, not durable.
|
|
163
|
+
|
|
164
|
+
summary: ONE neutral sentence naming the rule the feedback implies (or what the
|
|
165
|
+
user reacted to when there is no rule). Write it for every category.
|
|
166
|
+
confidence: your probability (0 to 1) that your durable-vs-not call is correct.
|
|
167
|
+
rationale: one short clause.
|
|
168
|
+
|
|
169
|
+
Respond with strict JSON matching the schema — no extra keys, no prose.
|
|
170
|
+
|
|
171
|
+
[source: {row["source_kind"]}]
|
|
172
|
+
{context}
|
|
173
|
+
=== FEEDBACK TO CLASSIFY ===
|
|
174
|
+
{row["text"]}"""
|
|
212
175
|
|
|
213
176
|
|
|
214
177
|
def build_fix_prompt(row: Mapping[str, object], context: str) -> str:
|
|
215
178
|
payload: dict[str, object] = json.loads(str(row["payload_json"]))
|
|
216
|
-
return
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
179
|
+
return f"""\
|
|
180
|
+
You are auditing one remark an AI coding assistant (Claude) made about an
|
|
181
|
+
automated hook that fired during its session, deciding whether the remark
|
|
182
|
+
REPORTS A MISFIRE — the hook firing wrongly or redundantly — or something else.
|
|
183
|
+
|
|
184
|
+
Pick exactly one category:
|
|
185
|
+
- misfire_confirmed: the remark asserts the hook fired wrongly — it re-fired on
|
|
186
|
+
content already addressed, flagged a false positive, or fired outside its
|
|
187
|
+
intended scope — and the surrounding conversation is consistent with that
|
|
188
|
+
claim.
|
|
189
|
+
- compliance: the remark acknowledges the hook's message and follows it (or
|
|
190
|
+
promises to follow it going forward).
|
|
191
|
+
- ambient_mention: the hook is merely described, quoted, or referenced in
|
|
192
|
+
passing, with no claim that it fired wrongly.
|
|
193
|
+
|
|
194
|
+
Only misfire_confirmed marks the hook as worth amending. A remark that both
|
|
195
|
+
complies and dismisses ("noted, but this re-fired on text I already fixed") is
|
|
196
|
+
misfire_confirmed — the dismissal is the signal. A remark that merely reports
|
|
197
|
+
the hook fired, or works around it without disputing it, is not.
|
|
198
|
+
|
|
199
|
+
summary: ONE neutral sentence naming what the hook did and what Claude claims
|
|
200
|
+
about it. Write it for every category.
|
|
201
|
+
confidence: your probability (0 to 1) that your misfire-vs-not call is correct.
|
|
202
|
+
rationale: one short clause.
|
|
203
|
+
|
|
204
|
+
Respond with strict JSON matching the schema — no extra keys, no prose.
|
|
205
|
+
|
|
206
|
+
[hook: {payload["target_hook_name"]} ({payload["event"]}/{payload["action"]})]
|
|
207
|
+
=== the hook's fire message ===
|
|
208
|
+
{payload["fire_message"]}
|
|
209
|
+
{context}
|
|
210
|
+
=== REMARK TO CLASSIFY ===
|
|
211
|
+
{row["text"]}"""
|
|
224
212
|
|
|
225
213
|
|
|
226
214
|
async def build_prompt(row: Mapping[str, object]) -> tuple[str, Fidelity]:
|