capt-hook 12.2.0__tar.gz → 12.3.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-12.2.0 → capt_hook-12.3.0}/PKG-INFO +1 -1
- {capt_hook-12.2.0 → capt_hook-12.3.0}/capt_hook_client/client.py +0 -1
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/.claude-plugin/plugin.json +1 -1
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/__init__.py +4 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/__init__.pyi +4 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/tombstones.py +1 -1
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/conditions.py +21 -4
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/context.py +21 -3
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/protocol.py +44 -21
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/signals/nlp.py +44 -23
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +4 -2
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +1 -1
- capt_hook-12.3.0/captain_hook/turn.py +32 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/types.py +23 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/pyproject.toml +1 -1
- {capt_hook-12.2.0 → capt_hook-12.3.0}/.gitignore +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/LICENSE +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/README.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/capt_hook_client/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/capt_hook_client/__main__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/capt_hook_client/key.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/__main__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/app.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/ast_grep.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/fixes/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/fixes/hooks/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/fixes/hooks/_lib.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/fixes/hooks/scratch_writes.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/fixes/hooks/teammate_permissions.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/_lib.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/commands.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/comments.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/deletions.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/detours.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/docs.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/models.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/plans.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/fragments/deliverable_rubric.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/fragments/workflow_script_header.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/browser_delegation_nudge.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/implementation_spawn_nudge.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/inline_edit_nudge.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/prose_spawn_gate.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/prose_workflow_nudge.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/review_routing_spawn_nudge.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/review_routing_workflow_nudge.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/writing_docs_spawn_nudge.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/models/writing_docs_workflow_nudge.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts/tools/preload_nudge.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/prompts.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/review.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/tasks.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/hooks/tools.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/general/pack.toml +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/go/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/go/hooks/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/go/hooks/testing.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/go/hooks/toolchain.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/performance/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/performance/hooks/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/performance/hooks/pipelining.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/python/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/python/hooks/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/python/hooks/style.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/python/hooks/testing.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/python/hooks/toolchain.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/steering/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/steering/hooks/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/steering/hooks/steering.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/steering/hooks/teammates.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/steering/hooks/workarounds.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/steering/pack.toml +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/classifiers/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/classifiers/conductor.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/cli.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/cmd.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/contexts.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/context.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/decision_writer.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/lifecycle.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/logsink.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/ops.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/registry.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/server.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/daemon/transcache.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/decisions.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/dispatch.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/doc_conventions.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/durable.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/events.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/fields.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/file.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/heartbeat.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/helper/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/helper/cli.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/helper/client.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/hooks/hooks.json +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/loader.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/log.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/packs/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/packs/manager.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/packs/plugins.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/commands.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/llm.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/permissions.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/provision.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/rewrite.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/primitives/workflow.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/prompt.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/py.typed +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/announce.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/cli.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/dashboard.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/fix.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/formats.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/judge.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/notify.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/pipeline.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/prompts.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/repo.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/routing.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/scan.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/settings.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/snapshot.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/status.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/store.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/sync.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/review/triage.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/session.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/settings.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/signals/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/state.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/style/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/style/ast_grep.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/style/matchers.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/style/scope.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/style/types.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/tasks.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/testing/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/testing/helpers.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/testing/types.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/transcripts.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/__init__.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/automode.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/caching.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/fs.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/globbing.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/http.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/model_cache.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/paths.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/proc.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/reqenv.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/scratch.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/shell.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/text.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/util/vcs.py +0 -0
- {capt_hook-12.2.0 → capt_hook-12.3.0}/hatch_build.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "captain-hook",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.2.0",
|
|
4
4
|
"displayName": "Captain Hook",
|
|
5
5
|
"description": "Skills for the capt-hook declarative hook framework: bootstrap gates and nudges from a repo's conventions, and translate style guides into enforced StyleRules.",
|
|
6
6
|
"author": {
|
|
@@ -46,6 +46,7 @@ EXPORTS: dict[str, str] = {
|
|
|
46
46
|
"Redirects": "captain_hook.conditions",
|
|
47
47
|
"RewritingExistingPlan": "captain_hook.conditions",
|
|
48
48
|
"ScratchPath": "captain_hook.conditions",
|
|
49
|
+
"UserSaid": "captain_hook.conditions",
|
|
49
50
|
"workflow_opt_matches": "captain_hook.conditions",
|
|
50
51
|
"workflow_opt_values": "captain_hook.conditions",
|
|
51
52
|
"workflow_script_source": "captain_hook.conditions",
|
|
@@ -122,6 +123,7 @@ EXPORTS: dict[str, str] = {
|
|
|
122
123
|
"Phrase": "captain_hook.signals.nlp",
|
|
123
124
|
"has_nominal_subject": "captain_hook.signals.nlp",
|
|
124
125
|
"is_past_predicate": "captain_hook.signals.nlp",
|
|
126
|
+
"subject_kind": "captain_hook.signals.nlp",
|
|
125
127
|
"HookState": "captain_hook.state",
|
|
126
128
|
"PrimitiveState": "captain_hook.state",
|
|
127
129
|
"WorkflowState": "captain_hook.state",
|
|
@@ -129,6 +131,7 @@ EXPORTS: dict[str, str] = {
|
|
|
129
131
|
"Task": "captain_hook.tasks",
|
|
130
132
|
"Tasks": "captain_hook.tasks",
|
|
131
133
|
"Allow": "captain_hook.testing.types",
|
|
134
|
+
"Turn": "captain_hook.turn",
|
|
132
135
|
"Ask": "captain_hook.testing.types",
|
|
133
136
|
"Block": "captain_hook.testing.types",
|
|
134
137
|
"FileFixture": "captain_hook.testing.types",
|
|
@@ -167,6 +170,7 @@ EXPORTS: dict[str, str] = {
|
|
|
167
170
|
"ToolInput": "captain_hook.types",
|
|
168
171
|
"TouchedFile": "captain_hook.types",
|
|
169
172
|
"UsedSkill": "captain_hook.types",
|
|
173
|
+
"UsedTool": "captain_hook.types",
|
|
170
174
|
"Waiting": "captain_hook.types",
|
|
171
175
|
"WorkflowScript": "captain_hook.types",
|
|
172
176
|
"binary_supports": "captain_hook.util.fs",
|
|
@@ -42,6 +42,7 @@ from captain_hook.conditions import Headless as Headless
|
|
|
42
42
|
from captain_hook.conditions import Redirects as Redirects
|
|
43
43
|
from captain_hook.conditions import RewritingExistingPlan as RewritingExistingPlan
|
|
44
44
|
from captain_hook.conditions import ScratchPath as ScratchPath
|
|
45
|
+
from captain_hook.conditions import UserSaid as UserSaid
|
|
45
46
|
from captain_hook.conditions import workflow_opt_matches as workflow_opt_matches
|
|
46
47
|
from captain_hook.conditions import workflow_opt_values as workflow_opt_values
|
|
47
48
|
from captain_hook.conditions import workflow_script_source as workflow_script_source
|
|
@@ -118,6 +119,7 @@ from captain_hook.signals.nlp import NlpSignal as NlpSignal
|
|
|
118
119
|
from captain_hook.signals.nlp import Phrase as Phrase
|
|
119
120
|
from captain_hook.signals.nlp import has_nominal_subject as has_nominal_subject
|
|
120
121
|
from captain_hook.signals.nlp import is_past_predicate as is_past_predicate
|
|
122
|
+
from captain_hook.signals.nlp import subject_kind as subject_kind
|
|
121
123
|
from captain_hook.state import HookState as HookState
|
|
122
124
|
from captain_hook.state import PrimitiveState as PrimitiveState
|
|
123
125
|
from captain_hook.state import WorkflowState as WorkflowState
|
|
@@ -133,6 +135,7 @@ from captain_hook.testing.types import Input as Input
|
|
|
133
135
|
from captain_hook.testing.types import Rewrite as Rewrite
|
|
134
136
|
from captain_hook.testing.types import TranscriptFixture as TranscriptFixture
|
|
135
137
|
from captain_hook.testing.types import Warn as Warn
|
|
138
|
+
from captain_hook.turn import Turn as Turn
|
|
136
139
|
from captain_hook.types import Action as Action
|
|
137
140
|
from captain_hook.types import Agent as Agent
|
|
138
141
|
from captain_hook.types import And as And
|
|
@@ -163,6 +166,7 @@ from captain_hook.types import Tool as Tool
|
|
|
163
166
|
from captain_hook.types import ToolInput as ToolInput
|
|
164
167
|
from captain_hook.types import TouchedFile as TouchedFile
|
|
165
168
|
from captain_hook.types import UsedSkill as UsedSkill
|
|
169
|
+
from captain_hook.types import UsedTool as UsedTool
|
|
166
170
|
from captain_hook.types import Waiting as Waiting
|
|
167
171
|
from captain_hook.types import WorkflowScript as WorkflowScript
|
|
168
172
|
from captain_hook.util.fs import binary_supports as binary_supports
|
|
@@ -33,6 +33,7 @@ from captain_hook.types import (
|
|
|
33
33
|
ToolInput,
|
|
34
34
|
TouchedFile,
|
|
35
35
|
UsedSkill,
|
|
36
|
+
UsedTool,
|
|
36
37
|
Waiting,
|
|
37
38
|
WorkflowScript,
|
|
38
39
|
)
|
|
@@ -44,6 +45,7 @@ if TYPE_CHECKING:
|
|
|
44
45
|
from cc_transcript.query import Session
|
|
45
46
|
|
|
46
47
|
from captain_hook.events import BaseHookEvent
|
|
48
|
+
from captain_hook.signals.nlp import Clause
|
|
47
49
|
from captain_hook.types import HookSpec
|
|
48
50
|
|
|
49
51
|
# Prose and config file extensions that shouldn't, on their own, demand a code-review pass.
|
|
@@ -183,13 +185,24 @@ def is_project_file(evt: BaseHookEvent) -> bool:
|
|
|
183
185
|
|
|
184
186
|
|
|
185
187
|
class UserSaid(CustomCondition):
|
|
186
|
-
"""Matches when
|
|
188
|
+
"""Matches when any user prompt in the session matches one of ``patterns``.
|
|
187
189
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
+
A string pattern is a case-insensitive regex, so plain keywords keep their
|
|
191
|
+
substring behavior; a :class:`~captain_hook.Clause` runs the dependency-clause
|
|
192
|
+
scan against each prompt. To match only the current turn's prompt, use
|
|
193
|
+
``evt.ctx.turn.matches`` instead.
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
>>> UserSaid("just commit", Clause(noun=Phrase("test"), verb=Phrase("skip")))
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __init__(self, *patterns: str | Clause) -> None:
|
|
200
|
+
self.patterns = patterns
|
|
190
201
|
|
|
191
202
|
def check(self, evt: BaseHookEvent) -> bool:
|
|
192
|
-
|
|
203
|
+
from captain_hook.signals.nlp import scan_text
|
|
204
|
+
|
|
205
|
+
return any(scan_text(turn.prompt, self.patterns) for turn in evt.ctx.t.turns if turn.prompt)
|
|
193
206
|
|
|
194
207
|
|
|
195
208
|
class AllEditsUnder(CustomCondition):
|
|
@@ -393,6 +406,10 @@ def check_condition(c: TCondition, evt: BaseHookEvent) -> bool:
|
|
|
393
406
|
)
|
|
394
407
|
case UsedSkill(names, subagents):
|
|
395
408
|
return has_used_skill(evt.ctx.transcript, names, subagents=subagents)
|
|
409
|
+
case UsedTool(names, subagents, scope):
|
|
410
|
+
return (evt.ctx.turn if scope == "turn" else evt.ctx.transcript).has_tool(
|
|
411
|
+
"|".join(names), subagents=subagents
|
|
412
|
+
)
|
|
396
413
|
case ReadFile(patterns, subagents):
|
|
397
414
|
return has_read_glob(evt.ctx.transcript, *patterns, subagents=subagents)
|
|
398
415
|
case TouchedFile(patterns, subagents):
|
|
@@ -17,6 +17,8 @@ if TYPE_CHECKING:
|
|
|
17
17
|
from spawnllm import TModel, TSpecialty
|
|
18
18
|
|
|
19
19
|
from captain_hook.settings import HooksSettings
|
|
20
|
+
from captain_hook.signals.nlp import Clause
|
|
21
|
+
from captain_hook.turn import Turn
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
RECENT_WINDOW = 15
|
|
@@ -70,15 +72,31 @@ class HookContext:
|
|
|
70
72
|
return self.conf
|
|
71
73
|
|
|
72
74
|
@cached_property
|
|
73
|
-
def turn(self) ->
|
|
74
|
-
"""The one-turn view of the current turn (cached)
|
|
75
|
-
|
|
75
|
+
def turn(self) -> Turn:
|
|
76
|
+
"""The one-turn view of the current turn (cached), with prompt matching via ``matches``."""
|
|
77
|
+
from captain_hook.turn import Turn
|
|
78
|
+
|
|
79
|
+
return Turn((current := self.transcript.current_turn).turns, current.path)
|
|
76
80
|
|
|
77
81
|
@cached_property
|
|
78
82
|
def prior(self) -> Session:
|
|
79
83
|
"""The session window before the current turn's last exchange (cached)."""
|
|
80
84
|
return self.transcript.prior()
|
|
81
85
|
|
|
86
|
+
def nlp(self, text: str, *patterns: str | Clause) -> bool:
|
|
87
|
+
"""Whether ``text`` matches any pattern — the escape hatch for matching arbitrary prose.
|
|
88
|
+
|
|
89
|
+
A string pattern is a case-insensitive regex; a :class:`~captain_hook.Clause`
|
|
90
|
+
runs the dependency-clause scan. For the current turn's prompt, prefer
|
|
91
|
+
``evt.ctx.turn.matches(*patterns)``.
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
>>> evt.ctx.nlp(evt.ctx.t.assistant_text(), Clause(noun=Phrase("test"), verb=Phrase("skip")))
|
|
95
|
+
"""
|
|
96
|
+
from captain_hook.signals.nlp import scan_text
|
|
97
|
+
|
|
98
|
+
return scan_text(text, patterns)
|
|
99
|
+
|
|
82
100
|
def transcript_text(self, *, window: int | None = None) -> str:
|
|
83
101
|
"""The transcript rendered turn by turn under the default budget.
|
|
84
102
|
|
|
@@ -104,7 +104,6 @@ class Request:
|
|
|
104
104
|
async_: bool = False
|
|
105
105
|
root: str | None = None
|
|
106
106
|
cwd: str | None = None
|
|
107
|
-
hooks: str | None = None
|
|
108
107
|
env: dict[str, str] = field(default_factory=dict)
|
|
109
108
|
payload_raw: str = ""
|
|
110
109
|
|
|
@@ -132,26 +131,50 @@ def decode_request(line: bytes) -> Request:
|
|
|
132
131
|
data = json.loads(line)
|
|
133
132
|
except (ValueError, UnicodeDecodeError) as exc:
|
|
134
133
|
raise ProtocolError(f"malformed request JSON: {exc}") from exc
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
134
|
+
if type(data) is not dict:
|
|
135
|
+
raise ProtocolError(f"invalid request shape: {data!r}")
|
|
136
|
+
kind = data.get("kind")
|
|
137
|
+
keys = {"v", "kind", "client"}
|
|
138
|
+
if kind == "event":
|
|
139
|
+
keys |= {"event", "async", "root", "cwd", "env", "payload_raw"}
|
|
140
|
+
if kind not in REQUEST_KINDS or set(data) != keys:
|
|
141
|
+
raise ProtocolError(f"invalid request shape: {data!r}")
|
|
142
|
+
client = data["client"]
|
|
143
|
+
if type(client) is not dict or set(client) != {"version", "build", "pid", "ppid"}:
|
|
144
|
+
raise ProtocolError(f"invalid request shape: {data!r}")
|
|
145
|
+
if not (
|
|
146
|
+
type(data["v"]) is int
|
|
147
|
+
and type(client["version"]) is str
|
|
148
|
+
and type(client["build"]) is str
|
|
149
|
+
and type(client["pid"]) is int
|
|
150
|
+
and type(client["ppid"]) is int
|
|
151
|
+
):
|
|
152
|
+
raise ProtocolError(f"invalid request shape: {data!r}")
|
|
153
|
+
info = ClientInfo(version=client["version"], build=client["build"], pid=client["pid"], ppid=client["ppid"])
|
|
154
|
+
if kind != "event":
|
|
155
|
+
return Request(v=data["v"], kind=kind, client=info)
|
|
156
|
+
env = data["env"]
|
|
157
|
+
if not (
|
|
158
|
+
type(data["event"]) is str
|
|
159
|
+
and type(data["async"]) is bool
|
|
160
|
+
and type(data["root"]) is str
|
|
161
|
+
and type(data["cwd"]) is str
|
|
162
|
+
and type(env) is dict
|
|
163
|
+
and all(type(key) is str and type(value) is str for key, value in env.items())
|
|
164
|
+
and type(data["payload_raw"]) is str
|
|
165
|
+
):
|
|
166
|
+
raise ProtocolError(f"invalid request shape: {data!r}")
|
|
167
|
+
return Request(
|
|
168
|
+
v=data["v"],
|
|
169
|
+
kind=kind,
|
|
170
|
+
client=info,
|
|
171
|
+
event=data["event"],
|
|
172
|
+
async_=data["async"],
|
|
173
|
+
root=data["root"],
|
|
174
|
+
cwd=data["cwd"],
|
|
175
|
+
env=env,
|
|
176
|
+
payload_raw=data["payload_raw"],
|
|
177
|
+
)
|
|
155
178
|
|
|
156
179
|
|
|
157
180
|
def encode_response(response: Response) -> bytes:
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import functools
|
|
4
|
+
import re
|
|
4
5
|
from collections.abc import Sequence
|
|
5
6
|
from dataclasses import dataclass
|
|
6
|
-
from typing import TYPE_CHECKING, Literal
|
|
7
|
+
from typing import TYPE_CHECKING, Literal, get_args
|
|
7
8
|
|
|
8
9
|
if TYPE_CHECKING:
|
|
9
10
|
from spacy.tokens import Doc, Span, Token
|
|
10
11
|
|
|
12
|
+
SubjectKind = Literal["unnamed", "passive", "actor"]
|
|
13
|
+
|
|
11
14
|
|
|
12
15
|
@dataclass(frozen=True, slots=True)
|
|
13
16
|
class PhraseFields:
|
|
@@ -96,18 +99,21 @@ class Clause:
|
|
|
96
99
|
are *not* past predicates and *not* counterfactual modal-perfects —
|
|
97
100
|
"will leave"/"leaving"/"leave it"/"will have left" but not
|
|
98
101
|
"left the workspace", "left to clean up", or "should have left".
|
|
99
|
-
subject:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"
|
|
107
|
-
|
|
102
|
+
subject: The subject shapes the verb may have (see ``subject_kind``), as
|
|
103
|
+
a tuple — ``()`` (default) applies no constraint, and a bare string
|
|
104
|
+
means that one shape. ``"unnamed"``: no substantive active subject —
|
|
105
|
+
imperatives ("switch back to plan mode"), pronoun subjects
|
|
106
|
+
("we should replan"), and true passives ("the file was removed").
|
|
107
|
+
``"passive"``: a substantive subject but no direct object —
|
|
108
|
+
elliptical passives ("config moved to settings.py") and intransitive
|
|
109
|
+
actives ("the parser crashed"). ``"actor"``: a substantive subject
|
|
110
|
+
acting on a direct object — described behavior like "the parser
|
|
111
|
+
removed the node". ``("unnamed",)`` matches directives but not
|
|
112
|
+
descriptions; ``("unnamed", "passive")`` also admits statements
|
|
113
|
+
about the thing acted on.
|
|
108
114
|
|
|
109
115
|
Example:
|
|
110
|
-
>>> Clause(verb=Phrase("remove", "delete"), tense="completed", subject="
|
|
116
|
+
>>> Clause(verb=Phrase("remove", "delete"), tense="completed", subject=("unnamed", "passive"))
|
|
111
117
|
"""
|
|
112
118
|
|
|
113
119
|
noun: Phrase | None = None
|
|
@@ -115,19 +121,23 @@ class Clause:
|
|
|
115
121
|
adj: Phrase | None = None
|
|
116
122
|
negated: bool = False
|
|
117
123
|
tense: Literal["any", "completed", "prospective"] = "any"
|
|
118
|
-
subject:
|
|
124
|
+
subject: SubjectKind | tuple[SubjectKind, ...] = ()
|
|
119
125
|
|
|
120
126
|
def __post_init__(self) -> None:
|
|
127
|
+
if isinstance(self.subject, str):
|
|
128
|
+
object.__setattr__(self, "subject", (self.subject,))
|
|
129
|
+
if invalid := set(self.subject) - set(get_args(SubjectKind)):
|
|
130
|
+
raise ValueError(f"Unknown subject kinds: {sorted(invalid)}")
|
|
121
131
|
if (anchor := self.noun or self.verb) is None:
|
|
122
132
|
raise ValueError("Clause needs a noun or verb anchor")
|
|
123
|
-
if self.verb is None and (self.tense != "any" or self.subject
|
|
133
|
+
if self.verb is None and (self.tense != "any" or self.subject):
|
|
124
134
|
raise ValueError("Clause tense and subject constraints require a verb")
|
|
125
135
|
if not (
|
|
126
136
|
(self.noun and self.verb)
|
|
127
137
|
or self.adj
|
|
128
138
|
or self.negated
|
|
129
139
|
or self.tense != "any"
|
|
130
|
-
or self.subject
|
|
140
|
+
or self.subject
|
|
131
141
|
or any(" " in lemma for lemma in anchor.lemmas)
|
|
132
142
|
):
|
|
133
143
|
raise ValueError("Clause needs a second constraint or a compound anchor phrase")
|
|
@@ -234,14 +244,18 @@ def tense_matches(tense: Literal["any", "completed", "prospective"], tok: Token)
|
|
|
234
244
|
return not is_past_predicate(tok) and not is_modal_perfect(tok)
|
|
235
245
|
|
|
236
246
|
|
|
237
|
-
def
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
247
|
+
def subject_kind(tok: Token) -> SubjectKind:
|
|
248
|
+
"""The shape of ``tok``'s subject, as :class:`Clause`'s ``subject`` constraint sees it.
|
|
249
|
+
|
|
250
|
+
``"unnamed"`` — no substantive active subject (see ``has_nominal_subject``);
|
|
251
|
+
``"actor"`` — a substantive subject plus a direct object; ``"passive"`` — a
|
|
252
|
+
substantive subject without one.
|
|
253
|
+
"""
|
|
254
|
+
if not has_nominal_subject(tok):
|
|
255
|
+
return "unnamed"
|
|
256
|
+
if any(c.dep_ == "dobj" for c in tok.children):
|
|
257
|
+
return "actor"
|
|
258
|
+
return "passive"
|
|
245
259
|
|
|
246
260
|
|
|
247
261
|
def verb_candidates(clause: Clause, sent: Span) -> list[Token]:
|
|
@@ -251,7 +265,7 @@ def verb_candidates(clause: Clause, sent: Span) -> list[Token]:
|
|
|
251
265
|
v
|
|
252
266
|
for v in find_lemma_matches(clause.verb, sent, {"VERB", "AUX"})
|
|
253
267
|
if tense_matches(clause.tense, v)
|
|
254
|
-
and
|
|
268
|
+
and (not clause.subject or subject_kind(v) in clause.subject)
|
|
255
269
|
and not (
|
|
256
270
|
clause.tense == "prospective"
|
|
257
271
|
and not clause.negated
|
|
@@ -278,3 +292,10 @@ def nlp_scan(clauses: Sequence[Clause], text: str) -> list[str]:
|
|
|
278
292
|
if not text.strip():
|
|
279
293
|
return []
|
|
280
294
|
return [sent.text.strip() for sent in parse(text).sents if any(match_clause(clause, sent) for clause in clauses)]
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def scan_text(text: str, patterns: Sequence[str | Clause]) -> bool:
|
|
298
|
+
clauses = [p for p in patterns if isinstance(p, Clause)]
|
|
299
|
+
return any(isinstance(p, str) and re.search(p, text, re.IGNORECASE) for p in patterns) or bool(
|
|
300
|
+
clauses and nlp_scan(clauses, text)
|
|
301
|
+
)
|
|
@@ -22,7 +22,7 @@ from captain_hook import (
|
|
|
22
22
|
Allow, And, Agent, Ask, BaseHookEvent, Block, Event, FilePath, FromSubagent,
|
|
23
23
|
HookResult, InlineTests, Input, Not, Or, Prompt, RanCommand, ReadFile, Rewrite, Runs,
|
|
24
24
|
Signal, Signals, SkipPermissions, SourceEdits, TestFile, Tool, ToolInput, TouchedFile,
|
|
25
|
-
TranscriptFixture, UsedSkill, Warn, WorkflowScript,
|
|
25
|
+
TranscriptFixture, UsedSkill, UsedTool, UserSaid, Warn, WorkflowScript,
|
|
26
26
|
approve, block_command, deny, gate, hook, lint, llm_approve, llm_gate, llm_nudge, nudge, on,
|
|
27
27
|
prompt_check, rewrite_command, set_tool_input, warn_command, workflow, Artifact, Step, text_matches,
|
|
28
28
|
)
|
|
@@ -165,6 +165,8 @@ evaluated first.
|
|
|
165
165
|
| Event comes from a subagent/teammate | `FromSubagent()` — the payload carries an `agent_id`; matches the ask's *origin*, where `Agent` matches its *type* |
|
|
166
166
|
| Session launched with bypass available | `SkipPermissions()` — walks to the nearest `claude` ancestor process and matches `--dangerously-skip-permissions` **or** `--allow-dangerously-skip-permissions`; availability counts as consent, whatever the active `permission_mode` |
|
|
167
167
|
| Skill was invoked | `UsedSkill("codex")` — bare name also matches `plugin:name` |
|
|
168
|
+
| Tool was previously used | `UsedTool("EnterPlanMode", scope="turn")` — exact names (a `\|`-joined string works); `scope="turn"` searches only the current turn |
|
|
169
|
+
| User said something | `UserSaid("just commit")` — case-insensitive regexes and/or `Clause`s over every user prompt |
|
|
168
170
|
| File was previously read | `ReadFile("TESTING.md")` — fnmatch globs; anchor dirs with `**/` |
|
|
169
171
|
| Match only test files | `TestFile()` (`**/test_*.py`, `**/*_test.py`, `**/conftest.py`, `**/tests/**/*.py`, `**/*_test.go`, `**/*.test.*`, `**/*.spec.*`) |
|
|
170
172
|
| Python source edits (skips tests by default, in-repo only) | `SourceEdits(lang="py")`; `lang` also `ts`, `go`, `rs`, ...; `project_only=False` to also match out-of-repo files |
|
|
@@ -177,7 +179,7 @@ evaluated first.
|
|
|
177
179
|
| Custom logic | implement `CustomCondition` |
|
|
178
180
|
<!-- /gen:conditions -->
|
|
179
181
|
|
|
180
|
-
`ReadFile`/`TouchedFile`/`RanCommand`/`UsedSkill` inspect the session transcript — they are
|
|
182
|
+
`ReadFile`/`TouchedFile`/`RanCommand`/`UsedSkill`/`UsedTool` inspect the session transcript — they are
|
|
181
183
|
how Stop gates know what already happened. Custom conditions are any object with a
|
|
182
184
|
`check(self, evt: BaseHookEvent) -> bool` method (a Protocol — no inheritance needed):
|
|
183
185
|
|
|
@@ -50,7 +50,7 @@ All fields are keyword-only — `Block(pattern="...")`, never `Block("...")`.
|
|
|
50
50
|
|
|
51
51
|
## TranscriptFixture recipe
|
|
52
52
|
|
|
53
|
-
Transcript-history conditions (`TouchedFile`, `RanCommand`, `ReadFile`, `UsedSkill`) read
|
|
53
|
+
Transcript-history conditions (`TouchedFile`, `RanCommand`, `ReadFile`, `UsedSkill`, `UsedTool`) read
|
|
54
54
|
the session transcript, so their tests supply one. The message shape is Claude Code JSONL:
|
|
55
55
|
`{"type": "assistant", "message": {"content": [<tool_use blocks>]}}`.
|
|
56
56
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from cc_transcript.query import Session
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from captain_hook.signals.nlp import Clause
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Turn(Session):
|
|
12
|
+
"""The current turn as a one-turn :class:`~cc_transcript.query.Session` view.
|
|
13
|
+
|
|
14
|
+
What ``evt.ctx.turn`` returns: the full windowed transcript surface
|
|
15
|
+
(``user_text``, ``has_tool``, ``tool_calls``, …) plus prompt matching, so
|
|
16
|
+
hooks can test the turn's opening prompt without importing the NLP machinery.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__slots__ = ()
|
|
20
|
+
|
|
21
|
+
def matches(self, *patterns: str | Clause) -> bool:
|
|
22
|
+
"""Whether the turn's opening prompt matches any pattern.
|
|
23
|
+
|
|
24
|
+
A string pattern is a case-insensitive regex; a
|
|
25
|
+
:class:`~captain_hook.Clause` runs the dependency-clause scan.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> evt.ctx.turn.matches(Clause(noun=Phrase("work"), verb=Phrase("stop", "halt")))
|
|
29
|
+
"""
|
|
30
|
+
from captain_hook.signals.nlp import scan_text
|
|
31
|
+
|
|
32
|
+
return scan_text(self.user_text, patterns)
|
|
@@ -328,6 +328,28 @@ class UsedSkill:
|
|
|
328
328
|
object.__setattr__(self, "subagents", subagents)
|
|
329
329
|
|
|
330
330
|
|
|
331
|
+
@dataclass(frozen=True, slots=True)
|
|
332
|
+
class UsedTool:
|
|
333
|
+
"""Transcript-history condition: true when a tool use named one of ``names`` exists.
|
|
334
|
+
|
|
335
|
+
Pass names variadically; a single ``|``-joined string still works for back-compat.
|
|
336
|
+
``scope="turn"`` restricts the search to the current turn — the idiom for skipping
|
|
337
|
+
a guard once the agent has complied.
|
|
338
|
+
|
|
339
|
+
Example:
|
|
340
|
+
>>> hook(Event.PreToolUse, skip_if=[UsedTool("EnterPlanMode", scope="turn")], message="...", block=True)
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
names: tuple[str, ...]
|
|
344
|
+
subagents: bool = True
|
|
345
|
+
scope: Literal["session", "turn"] = "session"
|
|
346
|
+
|
|
347
|
+
def __init__(self, *names: str, subagents: bool = True, scope: Literal["session", "turn"] = "session") -> None:
|
|
348
|
+
object.__setattr__(self, "names", _split_names(names))
|
|
349
|
+
object.__setattr__(self, "subagents", subagents)
|
|
350
|
+
object.__setattr__(self, "scope", scope)
|
|
351
|
+
|
|
352
|
+
|
|
331
353
|
@dataclass(frozen=True, slots=True)
|
|
332
354
|
class ReadFile(PatternsCondition):
|
|
333
355
|
"""Transcript-history condition: true when a Read tool use targeted a matching file.
|
|
@@ -676,6 +698,7 @@ TCondition = (
|
|
|
676
698
|
| FromSubagent
|
|
677
699
|
| SkipPermissions
|
|
678
700
|
| UsedSkill
|
|
701
|
+
| UsedTool
|
|
679
702
|
| ReadFile
|
|
680
703
|
| TestFile
|
|
681
704
|
| SourceEdits
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "capt-hook"
|
|
3
3
|
# Inert sentinel — the real version is stamped from the release tag (uv version --frozen).
|
|
4
|
-
version = "12.
|
|
4
|
+
version = "12.3.0"
|
|
5
5
|
description = "Stop repeating yourself to Claude."
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "PolyForm-Noncommercial-1.0.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/fixes/hooks/scratch_writes.py
RENAMED
|
File without changes
|
{capt_hook-12.2.0 → capt_hook-12.3.0}/captain_hook/builtin_packs/fixes/hooks/teammate_permissions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|