capt-hook 6.0.0__tar.gz → 6.2.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-6.0.0 → capt_hook-6.2.0}/PKG-INFO +1 -1
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/__init__.py +1 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/cli.py +4 -1
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/conditions.py +6 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/contexts.py +11 -6
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/events.py +22 -8
- capt_hook-6.2.0/captain_hook/packs/general/_lib.py +31 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/general/capt-hook.toml +2 -2
- capt_hook-6.2.0/captain_hook/packs/general/docs.py +158 -0
- capt_hook-6.2.0/captain_hook/packs/general/models.py +378 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/general/review.py +2 -28
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/general/tombstones.py +8 -1
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/primitives/llm.py +7 -2
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/signals/nlp.py +12 -7
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/skills/authoring-hooks/SKILL.md +3 -1
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +3 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +6 -3
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +1 -1
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/testing/helpers.py +31 -20
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/testing/types.py +4 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/pyproject.toml +1 -1
- capt_hook-6.0.0/captain_hook/packs/general/docs.py +0 -25
- capt_hook-6.0.0/captain_hook/packs/general/models.py +0 -221
- {capt_hook-6.0.0 → capt_hook-6.2.0}/LICENSE +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/README.md +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/.claude-plugin/plugin.json +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/__main__.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/app.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/ast_grep.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/classifiers/__init__.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/classifiers/conductor.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/command.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/context.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/decisions.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/dispatch.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/durable.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/fields.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/file.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/loader.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/log.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/__init__.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/general/__init__.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/general/commands.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/general/plans.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/general/prompts.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/general/tasks.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/go/__init__.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/go/capt-hook.toml +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/go/testing.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/go/toolchain.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/manager.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/python/__init__.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/python/capt-hook.toml +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/python/style.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/python/testing.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/python/toolchain.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/steering/__init__.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/steering/capt-hook.toml +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/packs/steering/steering.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/primitives/__init__.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/primitives/commands.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/primitives/rewrite.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/primitives/workflow.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/prompt.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/py.typed +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/review/__init__.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/review/cli.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/review/dashboard.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/review/fix.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/review/formats.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/review/judge.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/review/pipeline.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/review/repo.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/review/scan.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/review/settings.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/review/store.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/review/sync.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/session.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/settings.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/signals/__init__.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/state.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/style/__init__.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/style/ast_grep.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/style/matchers.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/style/scope.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/style/types.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/tasks.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/testing/__init__.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/types.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/util/__init__.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/util/fs.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/util/http.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/util/model_cache.py +0 -0
- {capt_hook-6.0.0 → capt_hook-6.2.0}/captain_hook/util/text.py +0 -0
|
@@ -23,6 +23,7 @@ from captain_hook import file, style, util
|
|
|
23
23
|
from captain_hook.app import hook, on
|
|
24
24
|
from captain_hook.ast_grep import COMMENT_TYPES
|
|
25
25
|
from captain_hook.command import CommandLine, ParsedCommand, Redirect
|
|
26
|
+
from captain_hook.conditions import workflow_opt_values, workflow_script_source
|
|
26
27
|
from captain_hook.context import HookContext
|
|
27
28
|
from captain_hook.contexts import AfterEdit, BeforeEdit, Introduced, PromptContext, apply_contexts
|
|
28
29
|
from captain_hook.durable import DurableSlot, DurableState, DurableStore
|
|
@@ -227,6 +227,9 @@ def print_hook_summary(label: str, summary: dict[str, str], deferred_to: str) ->
|
|
|
227
227
|
|
|
228
228
|
|
|
229
229
|
def provision_nlp(resolved: Sequence[manager.ResolvedPack]) -> None:
|
|
230
|
+
import httpx
|
|
231
|
+
import wn
|
|
232
|
+
|
|
230
233
|
from captain_hook.util import http
|
|
231
234
|
from captain_hook.util.model_cache import ensure_nlp_resources
|
|
232
235
|
|
|
@@ -235,7 +238,7 @@ def provision_nlp(resolved: Sequence[manager.ResolvedPack]) -> None:
|
|
|
235
238
|
click.echo("Provisioning NLP resources (spaCy en_core_web_sm ~13MB + oewn:2025 lexicon ~231MB, cached)...")
|
|
236
239
|
try:
|
|
237
240
|
ensure_nlp_resources()
|
|
238
|
-
except (http.GitHubFetchError, OSError) as e:
|
|
241
|
+
except (http.GitHubFetchError, OSError, wn.Error, httpx.HTTPError) as e:
|
|
239
242
|
click.echo(f" deferred (offline?): {e} — the SessionStart hook will retry at session start")
|
|
240
243
|
|
|
241
244
|
|
|
@@ -87,6 +87,12 @@ def is_waiting(evt: BaseHookEvent) -> bool:
|
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
def workflow_script_source(evt: BaseHookEvent) -> str | None:
|
|
90
|
+
"""The pending ``Workflow`` call's script source, or ``None``.
|
|
91
|
+
|
|
92
|
+
Returns the inline ``script`` when present, else reads the file at
|
|
93
|
+
``scriptPath``. A non-Workflow event, a missing/unreadable path, or a file
|
|
94
|
+
over ~1 MiB yields ``None``; never raises.
|
|
95
|
+
"""
|
|
90
96
|
if not (call := evt.as_input(WorkflowCall)):
|
|
91
97
|
return None
|
|
92
98
|
if call.script is not None:
|
|
@@ -43,12 +43,13 @@ class PromptContext(Protocol):
|
|
|
43
43
|
class BeforeEdit:
|
|
44
44
|
"""The pending edit's pre-image, as a ``<before_edit>`` block.
|
|
45
45
|
|
|
46
|
-
Reads :attr:`~captain_hook.events.ToolHookEvent.replaced` — an Edit's old text
|
|
47
|
-
MultiEdit's joined olds,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
Reads :attr:`~captain_hook.events.ToolHookEvent.replaced` — an Edit's old text or
|
|
47
|
+
a MultiEdit's joined olds at any event, a Write's current on-disk content only at
|
|
48
|
+
``PreToolUse`` (after the Write lands, disk holds the new text). The block is
|
|
49
|
+
omitted whenever the pre-image is unknowable: non-edit events, Writes off
|
|
50
|
+
``PreToolUse``, unreadable paths. Attached to every LLM primitive as a default
|
|
51
|
+
context; ``required=False`` because it is ambient enrichment. Pass
|
|
52
|
+
``BeforeEdit(required=True)`` explicitly to gate a hook on a non-empty pre-image.
|
|
52
53
|
"""
|
|
53
54
|
|
|
54
55
|
tag: str = "before_edit"
|
|
@@ -84,6 +85,10 @@ class Introduced:
|
|
|
84
85
|
see :data:`~captain_hook.ast_grep.COMMENT_TYPES`), ``pattern`` is an ast-grep
|
|
85
86
|
pattern. Extraction diffs the event's before/after text, so only constructs
|
|
86
87
|
absent before the edit appear; files without a supported language yield nothing.
|
|
88
|
+
The pre-image comes from ``evt.replaced``, so hooks covering Writes need
|
|
89
|
+
``events=Event.PreToolUse`` — at any other event a Write's pre-image is
|
|
90
|
+
unknowable, the context yields ``None``, and (being ``required``) the LLM call
|
|
91
|
+
skips rather than misreporting every construct as introduced.
|
|
87
92
|
|
|
88
93
|
``required`` defaults to ``True``: a context you attach explicitly IS the
|
|
89
94
|
evidence — no evidence, no LLM call. ``tag`` auto-derives from the class name in
|
|
@@ -36,6 +36,18 @@ if TYPE_CHECKING:
|
|
|
36
36
|
T = TypeVar("T", bound=ToolCallBase)
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
def disk_pre_image(path: Path) -> str | None:
|
|
40
|
+
if not path.exists():
|
|
41
|
+
return ""
|
|
42
|
+
if not path.is_file():
|
|
43
|
+
return None
|
|
44
|
+
try:
|
|
45
|
+
data = path.read_bytes()
|
|
46
|
+
except OSError:
|
|
47
|
+
return None
|
|
48
|
+
return data.decode(errors="replace")
|
|
49
|
+
|
|
50
|
+
|
|
39
51
|
@dataclass
|
|
40
52
|
class BaseHookEvent:
|
|
41
53
|
"""Base class for all hook events, providing access to raw payload, context, and convenience methods."""
|
|
@@ -233,18 +245,20 @@ class ToolHookEvent(BaseHookEvent):
|
|
|
233
245
|
|
|
234
246
|
@property
|
|
235
247
|
def replaced(self) -> str | None:
|
|
236
|
-
"""The pre-image of the text this call overwrites.
|
|
237
|
-
|
|
238
|
-
An Edit's ``old
|
|
239
|
-
file's current on-disk content
|
|
240
|
-
|
|
241
|
-
``PreToolUse``: once the Write lands, disk
|
|
248
|
+
"""The pre-image of the text this call overwrites, when knowable.
|
|
249
|
+
|
|
250
|
+
An Edit's ``old`` and a MultiEdit's newline-joined olds carry the pre-image at
|
|
251
|
+
any event. A Write's pre-image is the file's current on-disk content — ``""``
|
|
252
|
+
for a new file, lossily decoded for non-UTF-8 bytes, ``None`` for a directory
|
|
253
|
+
or unreadable path — and only at ``PreToolUse``: once the Write lands, disk
|
|
254
|
+
already holds the new text, so any other event yields ``None``. ``None`` for
|
|
255
|
+
other tools.
|
|
242
256
|
"""
|
|
243
257
|
match self.input:
|
|
244
258
|
case EditCall() | MultiEditCall():
|
|
245
259
|
return self.old
|
|
246
|
-
case WriteCall(file_path=path):
|
|
247
|
-
return
|
|
260
|
+
case WriteCall(file_path=path) if self.event is Event.PreToolUse:
|
|
261
|
+
return disk_pre_image(Path(path))
|
|
248
262
|
case _:
|
|
249
263
|
return None
|
|
250
264
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Conditions shared by the general pack's hook modules; the ``_`` prefix keeps the loader from registering it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from captain_hook import BaseHookEvent, CustomCondition
|
|
6
|
+
|
|
7
|
+
# Prose and config file extensions that shouldn't, on their own, demand a code-review pass.
|
|
8
|
+
# Tailor this (and the excluded dirs below) to scope what counts as "source" for your repo.
|
|
9
|
+
NON_SOURCE_SUFFIXES = (
|
|
10
|
+
".md",
|
|
11
|
+
".mdx",
|
|
12
|
+
".rst",
|
|
13
|
+
".txt",
|
|
14
|
+
".json",
|
|
15
|
+
".toml",
|
|
16
|
+
".yaml",
|
|
17
|
+
".yml",
|
|
18
|
+
".ini",
|
|
19
|
+
".cfg",
|
|
20
|
+
".lock",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EditedSource(CustomCondition):
|
|
25
|
+
"""True when the session edited a non-test source file (docs and config excluded)."""
|
|
26
|
+
|
|
27
|
+
def check(self, evt: BaseHookEvent) -> bool:
|
|
28
|
+
return any(
|
|
29
|
+
not f.is_test and f.suffix not in NON_SOURCE_SUFFIXES and not f.under("docs", ".claude", ".github")
|
|
30
|
+
for f in evt.ctx.t.tool_calls.named("Edit|Write").files()
|
|
31
|
+
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name = "general"
|
|
2
|
-
version = "0.
|
|
3
|
-
description = "Language-agnostic guards: git/command safety, doc & prompt nudges, task & plan discipline, and
|
|
2
|
+
version = "0.7.0"
|
|
3
|
+
description = "Language-agnostic guards: git/command safety, doc & prompt nudges, task & plan discipline, and code-review + docs-freshness gates."
|
|
4
4
|
hooks = "."
|
|
5
5
|
nlp = true
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from captain_hook import (
|
|
6
|
+
Allow,
|
|
7
|
+
BaseHookEvent,
|
|
8
|
+
Block,
|
|
9
|
+
CustomCondition,
|
|
10
|
+
Event,
|
|
11
|
+
FilePath,
|
|
12
|
+
Input,
|
|
13
|
+
Tool,
|
|
14
|
+
TouchedFile,
|
|
15
|
+
UsedSkill,
|
|
16
|
+
Waiting,
|
|
17
|
+
Warn,
|
|
18
|
+
llm_gate,
|
|
19
|
+
nudge,
|
|
20
|
+
)
|
|
21
|
+
from captain_hook.packs.general._lib import EditedSource
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Headless(CustomCondition):
|
|
25
|
+
"""True in a headless ``claude -p`` / SDK run (``CLAUDE_CODE_ENTRYPOINT`` in the ``sdk-*`` family)."""
|
|
26
|
+
|
|
27
|
+
def check(self, evt: BaseHookEvent) -> bool:
|
|
28
|
+
return os.environ.get("CLAUDE_CODE_ENTRYPOINT", "").startswith("sdk")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Advisory reminder to consult the writing-docs skill (and run slop-cop) before
|
|
32
|
+
# editing documentation. Fires once per session on the first doc edit and stands
|
|
33
|
+
# down once the skill has been used. Advisory only, so it never blocks an edit.
|
|
34
|
+
#
|
|
35
|
+
# The scaffolded .claude/settings.json registers the yasyf/cc-skills marketplace
|
|
36
|
+
# and enables writing-docs@skills, so the skill (and the skip_if check) activates
|
|
37
|
+
# when the folder is trusted — no manual /plugin install.
|
|
38
|
+
nudge(
|
|
39
|
+
"You're editing documentation. Consult the writing-docs skill first for the "
|
|
40
|
+
"Diataxis modes, voice rules, and code-sample rules, then run "
|
|
41
|
+
"`slop-cop check <file> --lang=markdown` to catch prose tells before you finish. "
|
|
42
|
+
"slop-cop is a Go binary — if it's not on PATH, run the `/slop-cop-check` skill "
|
|
43
|
+
"(it installs it), never `uvx slop-cop`.",
|
|
44
|
+
only_if=[Tool("Write|Edit"), FilePath("**/*.md", "**/*.qmd", "docs/**", "README.md")],
|
|
45
|
+
skip_if=[UsedSkill("writing-docs")],
|
|
46
|
+
max_fires=1,
|
|
47
|
+
tests={
|
|
48
|
+
Input(tool="Write", file="docs/guide/x.qmd", content="# X"): Warn(pattern="writing-docs"),
|
|
49
|
+
Input(tool="Edit", file="src/app.py", content="x = 1"): Allow(),
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Docs-freshness gate: after source edits, an LLM reads the uncommitted diff before the
|
|
55
|
+
# agent stops and blocks once when a user-facing change isn't reflected in README.md or
|
|
56
|
+
# docs/. Complements review.py's gate, which reviews correctness — this one reviews
|
|
57
|
+
# documentation. Stands down when the session already touched markdown/docs or used the
|
|
58
|
+
# writing-docs skill, and in headless (cron/CI) runs.
|
|
59
|
+
llm_gate(
|
|
60
|
+
"You are checking documentation freshness before the agent stops. The compact diff of "
|
|
61
|
+
"the uncommitted changes is in <diff>. Decide whether the session changed anything "
|
|
62
|
+
"user-facing — a new flag or option, a renamed command, changed output or behavior, a "
|
|
63
|
+
"new feature — that README.md or the pages under docs/ don't reflect. Set block=true "
|
|
64
|
+
"ONLY for a concrete gap, naming exactly which file and section to update in "
|
|
65
|
+
"`reasoning`. Otherwise block=false. Do not block on internal refactors, test or "
|
|
66
|
+
"tooling changes, or speculative staleness.",
|
|
67
|
+
message=lambda r: (
|
|
68
|
+
f"Docs freshness check found a gap to close before stopping: {r.reasoning} "
|
|
69
|
+
"Update README.md or docs/ via the writing-docs skill, or state that nothing "
|
|
70
|
+
"user-facing changed and finish."
|
|
71
|
+
),
|
|
72
|
+
diff=True,
|
|
73
|
+
only_if=[EditedSource()],
|
|
74
|
+
skip_if=[
|
|
75
|
+
Waiting(),
|
|
76
|
+
TouchedFile("**/*.md", "**/*.qmd"),
|
|
77
|
+
UsedSkill("writing-docs|writing-docs:writing-docs"),
|
|
78
|
+
Headless(),
|
|
79
|
+
],
|
|
80
|
+
events=Event.Stop,
|
|
81
|
+
max_fires=1,
|
|
82
|
+
tests={
|
|
83
|
+
Input(
|
|
84
|
+
transcript=[
|
|
85
|
+
{
|
|
86
|
+
"type": "assistant",
|
|
87
|
+
"message": {
|
|
88
|
+
"content": [
|
|
89
|
+
{
|
|
90
|
+
"type": "tool_use",
|
|
91
|
+
"name": "Edit",
|
|
92
|
+
"id": "e1",
|
|
93
|
+
"input": {"file_path": "/repo/src/app.py", "old_string": "a", "new_string": "b"},
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
]
|
|
99
|
+
): Block(pattern="writing-docs"),
|
|
100
|
+
Input(
|
|
101
|
+
transcript=[
|
|
102
|
+
{
|
|
103
|
+
"type": "assistant",
|
|
104
|
+
"message": {
|
|
105
|
+
"content": [
|
|
106
|
+
{
|
|
107
|
+
"type": "tool_use",
|
|
108
|
+
"name": "Edit",
|
|
109
|
+
"id": "e1",
|
|
110
|
+
"input": {"file_path": "/repo/src/app.py", "old_string": "a", "new_string": "b"},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"type": "tool_use",
|
|
114
|
+
"name": "Edit",
|
|
115
|
+
"id": "e2",
|
|
116
|
+
"input": {"file_path": "/repo/README.md", "old_string": "a", "new_string": "b"},
|
|
117
|
+
},
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
]
|
|
122
|
+
): Allow(),
|
|
123
|
+
Input(
|
|
124
|
+
transcript=[
|
|
125
|
+
{
|
|
126
|
+
"type": "assistant",
|
|
127
|
+
"message": {
|
|
128
|
+
"content": [
|
|
129
|
+
{
|
|
130
|
+
"type": "tool_use",
|
|
131
|
+
"name": "Edit",
|
|
132
|
+
"id": "e1",
|
|
133
|
+
"input": {"file_path": "/repo/README.md", "old_string": "a", "new_string": "b"},
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
]
|
|
139
|
+
): Allow(),
|
|
140
|
+
Input(
|
|
141
|
+
transcript=[
|
|
142
|
+
{
|
|
143
|
+
"type": "assistant",
|
|
144
|
+
"message": {
|
|
145
|
+
"content": [
|
|
146
|
+
{
|
|
147
|
+
"type": "tool_use",
|
|
148
|
+
"name": "Edit",
|
|
149
|
+
"id": "e1",
|
|
150
|
+
"input": {"file_path": "/repo/tests/test_app.py", "old_string": "a", "new_string": "b"},
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
]
|
|
156
|
+
): Allow(),
|
|
157
|
+
},
|
|
158
|
+
)
|