capt-hook 7.0.0__tar.gz → 8.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-7.0.0 → capt_hook-8.1.0}/PKG-INFO +1 -1
- capt_hook-8.1.0/captain_hook/packs/general/capt-hook.toml +5 -0
- capt_hook-8.1.0/captain_hook/packs/general/comments.py +160 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/general/models.py +145 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/general/prompts.py +10 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/general/tasks.py +15 -1
- capt_hook-8.1.0/captain_hook/packs/steering/capt-hook.toml +5 -0
- capt_hook-8.1.0/captain_hook/packs/steering/steering.py +731 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/primitives/commands.py +1 -1
- {capt_hook-7.0.0 → capt_hook-8.1.0}/pyproject.toml +1 -1
- capt_hook-7.0.0/captain_hook/packs/general/capt-hook.toml +0 -5
- capt_hook-7.0.0/captain_hook/packs/steering/capt-hook.toml +0 -5
- capt_hook-7.0.0/captain_hook/packs/steering/steering.py +0 -350
- {capt_hook-7.0.0 → capt_hook-8.1.0}/LICENSE +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/README.md +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/.claude-plugin/plugin.json +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/__init__.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/__main__.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/app.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/ast_grep.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/classifiers/__init__.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/classifiers/conductor.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/cli.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/conditions.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/context.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/contexts.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/decisions.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/dispatch.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/durable.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/events.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/fields.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/file.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/loader.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/log.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/__init__.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/fixes/__init__.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/fixes/capt-hook.toml +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/fixes/teammate_permissions.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/general/__init__.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/general/_lib.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/general/commands.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/general/detours.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/general/docs.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/general/plans.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/general/review.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/general/tombstones.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/go/__init__.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/go/capt-hook.toml +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/go/testing.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/go/toolchain.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/manager.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/python/__init__.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/python/capt-hook.toml +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/python/style.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/python/testing.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/python/toolchain.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/packs/steering/__init__.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/primitives/__init__.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/primitives/llm.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/primitives/permissions.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/primitives/rewrite.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/primitives/workflow.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/prompt.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/py.typed +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/review/__init__.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/review/cli.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/review/dashboard.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/review/fix.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/review/formats.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/review/judge.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/review/pipeline.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/review/repo.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/review/scan.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/review/settings.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/review/store.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/review/sync.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/session.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/settings.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/signals/__init__.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/signals/nlp.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/state.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/style/__init__.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/style/ast_grep.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/style/matchers.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/style/scope.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/style/types.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/tasks.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/testing/__init__.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/testing/helpers.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/testing/types.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/types.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/util/__init__.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/util/automode.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/util/fs.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/util/http.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/util/model_cache.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/util/proc.py +0 -0
- {capt_hook-7.0.0 → capt_hook-8.1.0}/captain_hook/util/text.py +0 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
name = "general"
|
|
2
|
+
version = "0.12.0"
|
|
3
|
+
description = "Language-agnostic guards: git/command safety, doc & prompt nudges, task & plan discipline, code-review + docs-freshness gates, a detour check-in nudge, a verbose-comment nudge, and delegated-prose nudges that catch doc writing handed to a subagent or workflow without the writing-docs skill."
|
|
4
|
+
hooks = "."
|
|
5
|
+
nlp = true
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Discourage verbose comments: warn when an edit introduces a long comment or line-comment run.
|
|
2
|
+
|
|
3
|
+
Comments should be terse and used sparingly — names, types, and organization carry the
|
|
4
|
+
meaning. The one legitimate exception is documentation-generation comments (godoc, rustdoc,
|
|
5
|
+
docstrings), and even a long doc run trips this warn by design: the threshold is deliberately
|
|
6
|
+
strict so verbosity of any kind gets a nudge. Language-agnostic via the tree-sitter comment
|
|
7
|
+
kinds in :data:`~captain_hook.ast_grep.COMMENT_TYPES`; diff-based, so only comments the edit
|
|
8
|
+
*introduces* count — a pre-existing long comment re-saved unchanged never fires.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from captain_hook import (
|
|
16
|
+
Allow,
|
|
17
|
+
BaseHookEvent,
|
|
18
|
+
CustomCondition,
|
|
19
|
+
Event,
|
|
20
|
+
FileFixture,
|
|
21
|
+
Input,
|
|
22
|
+
Tool,
|
|
23
|
+
Warn,
|
|
24
|
+
nudge,
|
|
25
|
+
)
|
|
26
|
+
from captain_hook.ast_grep import introduced_comments, lang_for_path
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from collections.abc import Iterable
|
|
30
|
+
|
|
31
|
+
from captain_hook.ast_grep import Match
|
|
32
|
+
|
|
33
|
+
MAX_COMMENT_LINES = 4
|
|
34
|
+
MAX_COMMENT_CHARS = 300
|
|
35
|
+
|
|
36
|
+
# Fixtures for the inline tests — kept as module constants so each physical line stays short.
|
|
37
|
+
GO_NO_COMMENT = "package p\n\nfunc F() {}\n"
|
|
38
|
+
GO_ONE_LINE_DOC = "package p\n\n// F does a thing.\nfunc F() {}\n"
|
|
39
|
+
GO_THREE_LINE_DOC = (
|
|
40
|
+
"package p\n\n// F does a thing.\n// It handles the empty case.\n// Returns an error otherwise.\nfunc F() {}\n"
|
|
41
|
+
)
|
|
42
|
+
GO_LONG_RUN = (
|
|
43
|
+
"package p\n\nfunc F() {\n"
|
|
44
|
+
"\t// explanation line one goes here\n"
|
|
45
|
+
"\t// explanation line two goes here\n"
|
|
46
|
+
"\t// explanation line three is here\n"
|
|
47
|
+
"\t// explanation line four is here\n"
|
|
48
|
+
"\t// explanation line five is here\n"
|
|
49
|
+
"\t// explanation line six is here ok\n"
|
|
50
|
+
"\tx := 1\n}\n"
|
|
51
|
+
)
|
|
52
|
+
GO_EXISTING_OLD = "func F() {\n\t// aaa\n\t// bbb\n\t// ccc\n\t// ddd\n\t// eee\n\t// fff\n\tx := 1\n}\n"
|
|
53
|
+
GO_EXISTING_NEW = "func F() {\n\t// aaa\n\t// bbb\n\t// ccc\n\t// ddd\n\t// eee\n\t// fff\n\tx := 2\n}\n"
|
|
54
|
+
GO_WRITE_PLAIN = "package p\n\nvar x = 1\n"
|
|
55
|
+
GO_WRITE_RUN = (
|
|
56
|
+
"package p\n\n"
|
|
57
|
+
"// note one here\n// note two here\n// note three here\n"
|
|
58
|
+
"// note four here\n// note five here\n// note six here\n"
|
|
59
|
+
"var x = 1\n"
|
|
60
|
+
)
|
|
61
|
+
GO_WRITE_EXISTING_OLD = "package p\n\n// aaa\n// bbb\n// ccc\n// ddd\n// eee\n// fff\nvar x = 1\n"
|
|
62
|
+
GO_WRITE_EXISTING_NEW = "package p\n\n// aaa\n// bbb\n// ccc\n// ddd\n// eee\n// fff\nvar x = 2\n"
|
|
63
|
+
RS_LONG_BLOCK = "/*\n one\n two\n three\n four\n five\n six\n seven\n eight\n*/\nfn f() {}\n"
|
|
64
|
+
RS_LONG_DOC = (
|
|
65
|
+
"/// line one of the rustdoc here\n"
|
|
66
|
+
"/// line two of the rustdoc here\n"
|
|
67
|
+
"/// line three of the rustdoc\n"
|
|
68
|
+
"/// line four of the rustdoc\n"
|
|
69
|
+
"/// line five of the rustdoc\n"
|
|
70
|
+
"/// line six of the rustdoc ok\n"
|
|
71
|
+
"pub fn f() {}\n"
|
|
72
|
+
)
|
|
73
|
+
PY_LONG_RUN = (
|
|
74
|
+
"# note one here\n# note two here\n# note three here\n# note four here\n# note five here\n# note six here\nx = 1\n"
|
|
75
|
+
)
|
|
76
|
+
PY_LONG_DOCSTRING = (
|
|
77
|
+
'def f():\n """\n'
|
|
78
|
+
" line one\n line two\n line three\n"
|
|
79
|
+
" line four\n line five\n line six\n"
|
|
80
|
+
' """\n return 1\n'
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def runs(matches: Iterable[Match]) -> list[list[Match]]:
|
|
85
|
+
"""Group comments into runs of adjacent lines — a block comment, or consecutive line
|
|
86
|
+
comments with no code between — each a list of :class:`Match` in document order."""
|
|
87
|
+
grouped: list[list[Match]] = []
|
|
88
|
+
for m in sorted(matches, key=lambda c: c.line):
|
|
89
|
+
if grouped and m.line <= grouped[-1][-1].end_line + 1:
|
|
90
|
+
grouped[-1].append(m)
|
|
91
|
+
else:
|
|
92
|
+
grouped.append([m])
|
|
93
|
+
return grouped
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def too_long(run: list[Match]) -> bool:
|
|
97
|
+
"""Whether a comment run exceeds the line or character budget."""
|
|
98
|
+
lines = run[-1].end_line - run[0].line + 1
|
|
99
|
+
chars = sum(len(m.text) for m in run)
|
|
100
|
+
return lines > MAX_COMMENT_LINES or chars > MAX_COMMENT_CHARS
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class LongCommentIntroduced(CustomCondition):
|
|
104
|
+
"""True when the pending edit introduces a comment (or adjacent line-comment run) past
|
|
105
|
+
the length budget. Diffs the edit's pre-image against its new text, so a comment already
|
|
106
|
+
present before the edit never counts; files whose language ast-grep can't parse yield False."""
|
|
107
|
+
|
|
108
|
+
def check(self, evt: BaseHookEvent) -> bool:
|
|
109
|
+
if (
|
|
110
|
+
not (file := evt.file)
|
|
111
|
+
or not (lang := lang_for_path(file.path))
|
|
112
|
+
or (old := evt.replaced) is None
|
|
113
|
+
or (new := evt.content) is None
|
|
114
|
+
):
|
|
115
|
+
return False
|
|
116
|
+
return any(too_long(run) for run in runs(introduced_comments(old, new, lang)))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
nudge(
|
|
120
|
+
"Verbose comment introduced. Comments should be terse and used sparingly — let names, "
|
|
121
|
+
"types, and organization document the code. Keep documentation-generation comments "
|
|
122
|
+
"(godoc / rustdoc / docstrings) to a real description, and drop long inline commentary "
|
|
123
|
+
"or anything that restates the code. See: STYLEGUIDE.md § Comments.",
|
|
124
|
+
only_if=[Tool("Edit", "Write", "MultiEdit"), LongCommentIntroduced()],
|
|
125
|
+
events=Event.PreToolUse,
|
|
126
|
+
tests={
|
|
127
|
+
# Short / within-budget comments — allowed.
|
|
128
|
+
Input(file="doc.go", old=GO_NO_COMMENT, content=GO_ONE_LINE_DOC): Allow(),
|
|
129
|
+
Input(file="doc.go", old=GO_NO_COMMENT, content=GO_THREE_LINE_DOC): Allow(),
|
|
130
|
+
Input(file="m.py", old="x = 1\n", content="# set x\nx = 1\n"): Allow(),
|
|
131
|
+
Input(file="lib.rs", old="pub fn f() {}\n", content="/// Builds a widget.\npub fn f() {}\n"): Allow(),
|
|
132
|
+
# Long introduced comment runs — warned.
|
|
133
|
+
Input(file="svc.go", old="package p\n\nfunc F() {\n\tx := 1\n}\n", content=GO_LONG_RUN): Warn(
|
|
134
|
+
pattern="Verbose comment"
|
|
135
|
+
), # 6-line // run > 4 lines
|
|
136
|
+
Input(file="big.go", old=GO_NO_COMMENT, content="package p\n\n// " + "x" * 320 + "\nfunc F() {}\n"): Warn(
|
|
137
|
+
pattern="Verbose comment"
|
|
138
|
+
), # single line, > 300 chars
|
|
139
|
+
Input(file="lib.rs", old="fn f() {}\n", content=RS_LONG_BLOCK): Warn(pattern="Verbose comment"),
|
|
140
|
+
Input(file="lib.rs", old="pub fn f() {}\n", content=RS_LONG_DOC): Warn(
|
|
141
|
+
pattern="Verbose comment"
|
|
142
|
+
), # long rustdoc run also warns (strict threshold, by design)
|
|
143
|
+
Input(file="m.py", old="x = 1\n", content=PY_LONG_RUN): Warn(pattern="Verbose comment"),
|
|
144
|
+
# Diff-gating: a pre-existing long run re-saved unchanged does not fire.
|
|
145
|
+
Input(file="svc.go", old=GO_EXISTING_OLD, content=GO_EXISTING_NEW): Allow(),
|
|
146
|
+
# Python docstrings are string nodes, not comments — never trip this.
|
|
147
|
+
Input(file="m.py", old="def f():\n return 1\n", content=PY_LONG_DOCSTRING): Allow(),
|
|
148
|
+
# Write tool: pre-image comes from disk at PreToolUse.
|
|
149
|
+
Input(
|
|
150
|
+
tool="Write",
|
|
151
|
+
file=FileFixture(name="cmt_new.go", content=GO_WRITE_PLAIN),
|
|
152
|
+
content=GO_WRITE_RUN,
|
|
153
|
+
): Warn(pattern="Verbose comment"), # long run added to existing file
|
|
154
|
+
Input(
|
|
155
|
+
tool="Write",
|
|
156
|
+
file=FileFixture(name="cmt_pre.go", content=GO_WRITE_EXISTING_OLD),
|
|
157
|
+
content=GO_WRITE_EXISTING_NEW,
|
|
158
|
+
): Allow(), # run already on disk: not introduced
|
|
159
|
+
},
|
|
160
|
+
)
|
|
@@ -732,3 +732,148 @@ Synthesis/accept-reject stays on fable.
|
|
|
732
732
|
): Allow(),
|
|
733
733
|
},
|
|
734
734
|
)
|
|
735
|
+
|
|
736
|
+
llm_nudge(
|
|
737
|
+
"""Decide whether this delegated subagent call delegates documentation or prose
|
|
738
|
+
writing without directing the subagent to read the writing-docs skill.
|
|
739
|
+
|
|
740
|
+
<delegated_spawn> holds the pending Agent/Task call: its model pin, agent type, and
|
|
741
|
+
prompt, ending with the sentences a clause prefilter matched — each asks a writing verb
|
|
742
|
+
of a prose artifact, with negated asks ("do NOT edit the docs") already screened out.
|
|
743
|
+
|
|
744
|
+
You are watching an orchestrating agent spawn a subagent. Decide whether the pending
|
|
745
|
+
prompt delegates documentation or prose writing — a README, docs page, CHANGELOG,
|
|
746
|
+
tutorial, release notes, or similar deliverable — without directing the subagent to
|
|
747
|
+
read the writing-docs skill. Restated style rules ('technical-builder voice', 'no hype
|
|
748
|
+
adjectives', 'first person, confident') do not count as reading the skill; that
|
|
749
|
+
paraphrase is exactly the failure to catch. Fire only when the prompt's deliverable is
|
|
750
|
+
prose the writing-docs skill governs. Do not fire for code work that incidentally
|
|
751
|
+
mentions a doc file, for reading or reviewing docs without writing them, or for a prompt
|
|
752
|
+
that already tells the agent to read the skill or its references.
|
|
753
|
+
|
|
754
|
+
When uncertain, fire=false — a false alarm teaches the agent to ignore this nudge. Keep
|
|
755
|
+
reasoning under 40 words.
|
|
756
|
+
|
|
757
|
+
<examples>
|
|
758
|
+
<example fire="true">
|
|
759
|
+
Rewrite the README for this repo. You are fable; technical-builder voice, no hype adjectives.
|
|
760
|
+
README prose with the style rules paraphrased in place of the skill — the drift this catches.
|
|
761
|
+
</example>
|
|
762
|
+
<example fire="true">
|
|
763
|
+
Draft the release notes for v2. Keep it first-person and confident, no marketing fluff.
|
|
764
|
+
Release-notes prose; restated voice rules, no pointer to the writing-docs skill.
|
|
765
|
+
</example>
|
|
766
|
+
<example fire="false">
|
|
767
|
+
Fix the failing test in cli.py; the README already documents the new flag.
|
|
768
|
+
Code work that only mentions the README — no prose is produced.
|
|
769
|
+
</example>
|
|
770
|
+
<example fire="false">
|
|
771
|
+
Rewrite the README, but read the doc-writing skill and its references first.
|
|
772
|
+
Already directs the subagent to the skill — nothing to nudge.
|
|
773
|
+
</example>
|
|
774
|
+
</examples>""",
|
|
775
|
+
message=lambda r: (
|
|
776
|
+
"This prompt delegates prose but paraphrases the writing rules instead of pointing at them. "
|
|
777
|
+
f"{r.reasoning} A paraphrase drifts and silently overrides the skill — rewrite the prompt to "
|
|
778
|
+
"direct the agent to READ the writing-docs skill and its references (the installed plugin under "
|
|
779
|
+
"~/.claude/plugins/cache/skills/writing-docs, or plugins/writing-docs in the cc-skills repo) "
|
|
780
|
+
"before it writes."
|
|
781
|
+
),
|
|
782
|
+
contexts=[ProseSpawn()],
|
|
783
|
+
events=Event.PreToolUse,
|
|
784
|
+
only_if=[Tool("Agent|Task")],
|
|
785
|
+
skip_if=[
|
|
786
|
+
ToolInput("prompt", r"(?i)writing-docs"),
|
|
787
|
+
Agent("Explore|claude-code-guide"),
|
|
788
|
+
],
|
|
789
|
+
max_context=16_000,
|
|
790
|
+
agent=False,
|
|
791
|
+
transcript=False,
|
|
792
|
+
tests={
|
|
793
|
+
Input(
|
|
794
|
+
prompt="Rewrite the README of /repo. You are fable; technical-builder voice, no hype "
|
|
795
|
+
"adjectives. Verify commands against the binary."
|
|
796
|
+
): Warn(pattern="writing-docs"),
|
|
797
|
+
Input(
|
|
798
|
+
prompt="Rewrite the README of /repo; technical-builder voice, no hype adjectives. Read "
|
|
799
|
+
"the writing-docs skill at ~/.claude/plugins/cache/skills/writing-docs first."
|
|
800
|
+
): Allow(),
|
|
801
|
+
Input(prompt="Fix the race in daemon.go; update the failing test"): Allow(),
|
|
802
|
+
Input(
|
|
803
|
+
prompt="Rewrite the README, but read the doc-writing skill and its references first",
|
|
804
|
+
llm={"fire": False},
|
|
805
|
+
): Allow(),
|
|
806
|
+
},
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
llm_nudge(
|
|
810
|
+
"""Decide whether this workflow script delegates documentation or prose writing to an
|
|
811
|
+
agent() stage without directing that subagent to read the writing-docs skill.
|
|
812
|
+
|
|
813
|
+
<workflow_script> holds the pending Workflow call's script source, headed by every line
|
|
814
|
+
that pins a model, followed by the sentences a clause prefilter matched — each asks a
|
|
815
|
+
writing verb of a prose artifact, with negated asks ("do NOT edit CHANGELOG.md") already
|
|
816
|
+
screened out.
|
|
817
|
+
|
|
818
|
+
Decide whether an agent() prompt delegates documentation or prose writing — a README,
|
|
819
|
+
docs page, CHANGELOG, tutorial, release notes, or similar deliverable — without
|
|
820
|
+
directing that subagent to read the writing-docs skill. Restated style rules
|
|
821
|
+
('technical-builder voice', 'no hype adjectives', 'first person, confident') do not
|
|
822
|
+
count as reading the skill; that paraphrase is exactly the failure to catch. Fire only
|
|
823
|
+
when a stage's deliverable is prose the writing-docs skill governs. Do not fire for code
|
|
824
|
+
work that incidentally mentions a doc file, for reading or reviewing docs without writing
|
|
825
|
+
them, or for a stage that already tells its subagent to read the skill or its references.
|
|
826
|
+
|
|
827
|
+
When uncertain, fire=false — a false alarm teaches the agent to ignore this nudge. Keep
|
|
828
|
+
reasoning under 40 words and name the offending stage.
|
|
829
|
+
|
|
830
|
+
<examples>
|
|
831
|
+
<example fire="true">
|
|
832
|
+
agent('Rewrite the README for the new CLI. Technical-builder voice, no hype adjectives', {model: 'opus'})
|
|
833
|
+
The stage's deliverable is README prose with the style rules paraphrased in place of the skill.
|
|
834
|
+
</example>
|
|
835
|
+
<example fire="true">
|
|
836
|
+
agent(`Draft the docs-site page for ${feature}. First-person, confident, no marketing fluff`)
|
|
837
|
+
A docs page delegated with restated voice rules and no pointer to the writing-docs skill.
|
|
838
|
+
</example>
|
|
839
|
+
<example fire="false">
|
|
840
|
+
agent('Fix the failing import in cli.py; the README already documents the flag', {model: 'opus'})
|
|
841
|
+
Code work that only mentions the README — no prose is produced.
|
|
842
|
+
</example>
|
|
843
|
+
<example fire="false">
|
|
844
|
+
agent('Rewrite the troubleshooting guide, but read the doc-writing skill and its references first')
|
|
845
|
+
The stage already directs its subagent to the skill — nothing to nudge.
|
|
846
|
+
</example>
|
|
847
|
+
</examples>""",
|
|
848
|
+
message=lambda r: (
|
|
849
|
+
"This workflow script delegates prose but paraphrases the writing rules instead of pointing at "
|
|
850
|
+
f"them. {r.reasoning} A paraphrase drifts and silently overrides the skill — rewrite the offending "
|
|
851
|
+
"agent() prompt to direct its subagent to READ the writing-docs skill and its references (the "
|
|
852
|
+
"installed plugin under ~/.claude/plugins/cache/skills/writing-docs, or plugins/writing-docs in "
|
|
853
|
+
"the cc-skills repo) before it writes."
|
|
854
|
+
),
|
|
855
|
+
contexts=[ProseWorkflowScript()],
|
|
856
|
+
events=Event.PreToolUse,
|
|
857
|
+
only_if=[Tool("Workflow")],
|
|
858
|
+
skip_if=[WorkflowScript(pattern=r"(?i)writing-docs")],
|
|
859
|
+
max_fires=2,
|
|
860
|
+
max_context=16_000,
|
|
861
|
+
agent=False,
|
|
862
|
+
transcript=False,
|
|
863
|
+
tests={
|
|
864
|
+
Input(
|
|
865
|
+
script="agent('Rewrite the README. You are fable; technical-builder voice, no hype "
|
|
866
|
+
"adjectives', {model: 'opus'})"
|
|
867
|
+
): Warn(pattern="writing-docs"),
|
|
868
|
+
Input(
|
|
869
|
+
script="agent('Rewrite the README per the writing-docs skill at "
|
|
870
|
+
"~/.claude/plugins/cache/skills/writing-docs', {model: 'opus'})"
|
|
871
|
+
): Allow(),
|
|
872
|
+
Input(script="agent('Fix the race in daemon.go; update the failing test')"): Allow(),
|
|
873
|
+
Input(
|
|
874
|
+
script="agent('Rewrite the README, but read the doc-writing skill and its references "
|
|
875
|
+
"first', {model: 'opus'})",
|
|
876
|
+
llm={"fire": False},
|
|
877
|
+
): Allow(),
|
|
878
|
+
},
|
|
879
|
+
)
|
|
@@ -24,6 +24,7 @@ nudge(
|
|
|
24
24
|
skip_if=[
|
|
25
25
|
UsedSkill("llm-prompts"),
|
|
26
26
|
UsedSkill("slop-cop-check", "slop-cop-prose"),
|
|
27
|
+
Content(r"\.count_tokens\(", project_only=False),
|
|
27
28
|
],
|
|
28
29
|
tests={
|
|
29
30
|
Input(
|
|
@@ -31,5 +32,14 @@ nudge(
|
|
|
31
32
|
): Warn(),
|
|
32
33
|
Input(file="prompt.md", content="<instruction>\nSummarize the document.\n</instruction>\n"): Warn(),
|
|
33
34
|
Input(file="util.py", content="def add(a, b):\n return a + b\n"): Allow(),
|
|
35
|
+
Input(
|
|
36
|
+
file="tokens.py",
|
|
37
|
+
content=(
|
|
38
|
+
"def api_count(text: str, model: str) -> int:\n"
|
|
39
|
+
" resp = client.messages.count_tokens(\n"
|
|
40
|
+
' model=model,\n messages=[{"role": "user", "content": text or " "}],\n'
|
|
41
|
+
" )\n return int(resp.input_tokens)\n"
|
|
42
|
+
),
|
|
43
|
+
): Allow(),
|
|
34
44
|
},
|
|
35
45
|
)
|
|
@@ -55,6 +55,13 @@ class DriftedFromTasks(CustomCondition):
|
|
|
55
55
|
return since.tool_calls.named("Bash|Grep|Glob|WebSearch|WebFetch|LSP|Skill").count() >= TASK_DRIFT_THRESHOLD
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
class TaskNotification(CustomCondition):
|
|
59
|
+
"""Matches when the prompt is a system-injected background-task report, not free-form user text."""
|
|
60
|
+
|
|
61
|
+
def check(self, evt: BaseHookEvent) -> bool:
|
|
62
|
+
return (evt.user_prompt or "").strip().startswith("<task-notification>")
|
|
63
|
+
|
|
64
|
+
|
|
58
65
|
gate(
|
|
59
66
|
"Open tasks remain. Before stopping, mark each finished task status='completed' via the "
|
|
60
67
|
"TaskUpdate tool (add a note if you're deliberately deferring one), or output "
|
|
@@ -120,7 +127,7 @@ nudge(
|
|
|
120
127
|
nudge(
|
|
121
128
|
"This message has several distinct requests. Use the TaskCreate tool for each item "
|
|
122
129
|
"before starting work, so none gets dropped. See: CLAUDE.md § Task Tracking.",
|
|
123
|
-
skip_if=[InPlanMode()],
|
|
130
|
+
skip_if=[InPlanMode(), TaskNotification()],
|
|
124
131
|
events=Event.UserPromptSubmit,
|
|
125
132
|
signals=Signals(
|
|
126
133
|
[
|
|
@@ -141,5 +148,12 @@ nudge(
|
|
|
141
148
|
Input(prompt="1. add foo\n2. fix bar\n3. update baz"): Warn(),
|
|
142
149
|
Input(prompt="just fix the typo"): Allow(),
|
|
143
150
|
Input(prompt="1. add foo\n2. fix bar\n3. update baz", permission_mode="plan"): Allow(),
|
|
151
|
+
Input(
|
|
152
|
+
prompt=(
|
|
153
|
+
"<task-notification>\n<task-id>abc</task-id>\n<status>completed</status>\n"
|
|
154
|
+
"<summary>Agent finished</summary>\n<result>\n- `cc-transcript` -> version 7.0.1\n"
|
|
155
|
+
"- `spawnllm` -> version 0.5.2\n</result>\n</task-notification>"
|
|
156
|
+
)
|
|
157
|
+
): Allow(),
|
|
144
158
|
},
|
|
145
159
|
)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
name = "steering"
|
|
2
|
+
version = "0.4.0"
|
|
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, and don't defer the requested fix by swapping in docs or a follow-up nobody asked for."
|
|
4
|
+
hooks = "."
|
|
5
|
+
nlp = true
|