capt-hook 12.76.2__tar.gz → 12.77.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.76.2 → capt_hook-12.77.0}/PKG-INFO +1 -1
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/__init__.py +3 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/__init__.pyi +3 -0
- capt_hook-12.77.0/captain_hook/annotations.py +296 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/app.py +13 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/graphite/hooks/_lib.py +1 -11
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/graphite/hooks/queue.py +2 -3
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/graphite/hooks/vcs.py +26 -12
- capt_hook-12.77.0/captain_hook/confirm.py +178 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/dispatch.py +8 -4
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/events.py +30 -2
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/testing/helpers.py +1 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/types.py +5 -1
- {capt_hook-12.76.2 → capt_hook-12.77.0}/pyproject.toml +1 -1
- {capt_hook-12.76.2 → capt_hook-12.77.0}/.gitignore +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/LICENSE +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/README.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/capt_hook_client/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/capt_hook_client/__main__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/capt_hook_client/client.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/.claude-plugin/plugin.json +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/.mcp.json +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/__main__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/ast_grep.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/bindings.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/fixes/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/fixes/hooks/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/fixes/hooks/_lib.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/fixes/hooks/scratch_writes.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/fixes/hooks/teammate_permissions.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/_lib.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/_sessions.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/commands.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/comments.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/deletions.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/detours.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/docs.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/hook_files.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/models.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/plain_english.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/plans.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/fragments/deliverable_rubric.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/fragments/workflow_script_header.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/browser_delegation_nudge.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/implementation_spawn_nudge.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/inline_edit_nudge.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/prose_spawn_gate.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/prose_workflow_nudge.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/review_routing_spawn_nudge.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/review_routing_workflow_nudge.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/writing_docs_spawn_nudge.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/models/writing_docs_workflow_nudge.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/plain_english_rules.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts/tools/preload_nudge.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/prompts.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/questions.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/review.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/sessions.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/tasks.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/tombstones.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/tooling.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/hooks/tools.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/general/pack.toml +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/go/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/go/hooks/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/go/hooks/testing.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/go/hooks/toolchain.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/graphite/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/graphite/gitdir/.graphite_repo_config +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/graphite/hooks/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/performance/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/performance/hooks/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/performance/hooks/filesystem.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/performance/hooks/pipelining.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/python/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/python/hooks/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/python/hooks/style.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/python/hooks/testing.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/python/hooks/toolchain.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/steering/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/steering/hooks/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/steering/hooks/steering.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/steering/hooks/teammates.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/steering/hooks/workarounds.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/builtin_packs/steering/pack.toml +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/classifiers/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/classifiers/conductor.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/classifiers/lane.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/cli.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/cmd.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/command_schema.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/command_schemas.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/conditions.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/context.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/contexts.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/daemon/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/daemon/context.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/daemon/decision_writer.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/daemon/logsink.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/daemon/registry.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/daemon/tree.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/decisions.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/desktop/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/desktop/cli.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/desktop/client.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/doc_conventions.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/durable.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/faults.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/fields.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/file.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/guard_literal.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/guard_literal.sh +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/heartbeat.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/hook_lint.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/hooks/hooks.json +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/loader.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/log.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/mcp_server.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/packs/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/packs/manager.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/packs/plugins.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/commands.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/llm.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/permissions.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/provision.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/rewrite.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/primitives/workflow.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/prompt.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/py.typed +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/announce.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/cli.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/dashboard.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/fix.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/formats.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/judge.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/notify.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/pipeline.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/prompts.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/repo.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/routing.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/scan.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/settings.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/snapshot.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/status.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/store.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/sync.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/review/triage.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/scripts/install-binary.sh +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/scripts/install-mcp.sh +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/session.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/settings.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/signals/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/signals/nlp.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/client.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/contracts.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/review.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/schema.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/schemas/config.schema.json +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/schemas/context.schema.json +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/schemas/host-request.schema.json +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/schemas/host-response.schema.json +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/schemas/request.schema.json +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/schemas/response.schema.json +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/schemas/tool_registry.schema.json +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/validation.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/snapshots/worker.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/state.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/style/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/style/ast_grep.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/style/matchers.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/style/scope.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/style/types.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/tasks.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/testing/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/testing/fixtures.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/testing/snapshots.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/testing/types.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/transcripts.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/turn.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/update/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/update/cli.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/update/settings.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/update/updater.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/automode.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/caching.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/fs.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/globbing.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/http.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/model_cache.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/paths.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/payload.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/proc.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/reqenv.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/scratch.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/shell.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/text.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/userpath.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/util/vcs.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/worker/__init__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/worker/__main__.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/worker/fail_open.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/worker/protocol.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/worker/runtime.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/captain_hook/worker/service.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/hatch_build.py +0 -0
- {capt_hook-12.76.2 → capt_hook-12.77.0}/internal/wireproto/guard.json +0 -0
|
@@ -30,6 +30,7 @@ EXPORTS: dict[str, str] = {
|
|
|
30
30
|
"util": "captain_hook.util",
|
|
31
31
|
"hook": "captain_hook.app",
|
|
32
32
|
"on": "captain_hook.app",
|
|
33
|
+
"Annotated": "captain_hook.annotations",
|
|
33
34
|
"COMMENT_TYPES": "captain_hook.ast_grep",
|
|
34
35
|
"Edit": "captain_hook.ast_grep",
|
|
35
36
|
"Match": "captain_hook.ast_grep",
|
|
@@ -59,6 +60,8 @@ EXPORTS: dict[str, str] = {
|
|
|
59
60
|
"workflow_opt_matches": "captain_hook.conditions",
|
|
60
61
|
"workflow_opt_values": "captain_hook.conditions",
|
|
61
62
|
"workflow_script_source": "captain_hook.conditions",
|
|
63
|
+
"Confirm": "captain_hook.confirm",
|
|
64
|
+
"ConfirmVerdict": "captain_hook.confirm",
|
|
62
65
|
"HookContext": "captain_hook.context",
|
|
63
66
|
"AfterEdit": "captain_hook.contexts",
|
|
64
67
|
"BeforeEdit": "captain_hook.contexts",
|
|
@@ -24,6 +24,7 @@ from cc_transcript.tools import WriteCall as WriteCall
|
|
|
24
24
|
from captain_hook import file as file
|
|
25
25
|
from captain_hook import style as style
|
|
26
26
|
from captain_hook import util as util
|
|
27
|
+
from captain_hook.annotations import Annotated as Annotated
|
|
27
28
|
from captain_hook.app import hook as hook
|
|
28
29
|
from captain_hook.app import on as on
|
|
29
30
|
from captain_hook.ast_grep import COMMENT_TYPES as COMMENT_TYPES
|
|
@@ -55,6 +56,8 @@ from captain_hook.conditions import UserSaid as UserSaid
|
|
|
55
56
|
from captain_hook.conditions import workflow_opt_matches as workflow_opt_matches
|
|
56
57
|
from captain_hook.conditions import workflow_opt_values as workflow_opt_values
|
|
57
58
|
from captain_hook.conditions import workflow_script_source as workflow_script_source
|
|
59
|
+
from captain_hook.confirm import Confirm as Confirm
|
|
60
|
+
from captain_hook.confirm import ConfirmVerdict as ConfirmVerdict
|
|
58
61
|
from captain_hook.context import HookContext as HookContext
|
|
59
62
|
from captain_hook.contexts import AfterEdit as AfterEdit
|
|
60
63
|
from captain_hook.contexts import BeforeEdit as BeforeEdit
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""The ``ccx:`` annotation notation: one escape and intent grammar for every hook to read."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Iterator, Mapping
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from itertools import chain
|
|
9
|
+
from typing import TYPE_CHECKING, Literal
|
|
10
|
+
|
|
11
|
+
from cc_transcript.command import PAYLOAD_DEPTH_LIMIT
|
|
12
|
+
from cc_transcript.tools import SkillCall, TaskCall
|
|
13
|
+
|
|
14
|
+
from captain_hook.cmd import Cmd
|
|
15
|
+
from captain_hook.snapshots.client import RemoteSession
|
|
16
|
+
from captain_hook.types import CustomCondition
|
|
17
|
+
from captain_hook.util import reqenv
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from captain_hook.cmd import Call
|
|
21
|
+
from captain_hook.events import BaseHookEvent
|
|
22
|
+
|
|
23
|
+
WORD = r"[a-z0-9][a-z0-9:._-]*"
|
|
24
|
+
COMMENT_TOKEN = re.compile(rf"ccx:({WORD})(?:=({WORD}))?")
|
|
25
|
+
DISPATCH_LINE = re.compile(rf"ccx:((?:[ \t]+{WORD}(?:={WORD})?)+)")
|
|
26
|
+
DISPATCH_PAIR = re.compile(rf"({WORD})(?:=({WORD}))?")
|
|
27
|
+
RAW_ENV = "CAPT_HOOK_CCX_RAW"
|
|
28
|
+
TRUTHY = frozenset({"1", "true", "yes"})
|
|
29
|
+
BREAKS = frozenset(" \t\n;&|()<>")
|
|
30
|
+
SHELLS = frozenset({"sh", "bash", "zsh", "dash", "ksh"})
|
|
31
|
+
OPTION_VALUES = frozenset({"-o", "+o", "-O", "+O"})
|
|
32
|
+
FENCE = re.compile(r"(`{3,}|~{3,})")
|
|
33
|
+
|
|
34
|
+
type Annotations = Mapping[str, str | None]
|
|
35
|
+
type Pair = tuple[str, str | None]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def pair(key: str, value: str) -> Pair:
|
|
39
|
+
return key, value or None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class CommentScanner:
|
|
44
|
+
"""Bash's own reading of where comments sit: quoting, line continuations, and every pending heredoc body."""
|
|
45
|
+
|
|
46
|
+
text: str
|
|
47
|
+
at: int = 0
|
|
48
|
+
found: list[str] = field(default_factory=list)
|
|
49
|
+
heredocs: list[tuple[str, bool]] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
def peek(self, offset: int = 0) -> str:
|
|
52
|
+
return self.text[self.at + offset : self.at + offset + 1]
|
|
53
|
+
|
|
54
|
+
def scan(self, close: str = "") -> None:
|
|
55
|
+
word_start, depth = True, 0
|
|
56
|
+
while self.at < len(self.text):
|
|
57
|
+
char = self.peek()
|
|
58
|
+
if char == close and depth == 0:
|
|
59
|
+
self.at += 1
|
|
60
|
+
return
|
|
61
|
+
match char:
|
|
62
|
+
case "\\" if self.peek(1) == "\n":
|
|
63
|
+
self.at += 2
|
|
64
|
+
case "\\":
|
|
65
|
+
self.at += 2
|
|
66
|
+
word_start = False
|
|
67
|
+
case "'":
|
|
68
|
+
self.skip_to("'")
|
|
69
|
+
word_start = False
|
|
70
|
+
case '"':
|
|
71
|
+
self.skip_double()
|
|
72
|
+
word_start = False
|
|
73
|
+
case "#" if word_start:
|
|
74
|
+
end = self.text.find("\n", self.at)
|
|
75
|
+
self.found.append(self.text[self.at : end if end >= 0 else None])
|
|
76
|
+
self.at = end if end >= 0 else len(self.text)
|
|
77
|
+
case "\n":
|
|
78
|
+
self.at += 1
|
|
79
|
+
self.read_heredocs()
|
|
80
|
+
word_start = True
|
|
81
|
+
case "$":
|
|
82
|
+
self.expansion()
|
|
83
|
+
word_start = False
|
|
84
|
+
case "`":
|
|
85
|
+
self.at += 1
|
|
86
|
+
self.scan("`")
|
|
87
|
+
word_start = False
|
|
88
|
+
case "<" if self.text.startswith("<<", self.at) and not self.text.startswith("<<<", self.at):
|
|
89
|
+
self.heredoc()
|
|
90
|
+
word_start = False
|
|
91
|
+
case _:
|
|
92
|
+
depth += (char == "(") - (char == ")") if close == ")" else 0
|
|
93
|
+
self.at += 1
|
|
94
|
+
word_start = char in BREAKS
|
|
95
|
+
|
|
96
|
+
def skip_to(self, quote: str) -> None:
|
|
97
|
+
end = self.text.find(quote, self.at + 1)
|
|
98
|
+
self.at = end + 1 if end >= 0 else len(self.text)
|
|
99
|
+
|
|
100
|
+
def skip_double(self) -> None:
|
|
101
|
+
self.at += 1
|
|
102
|
+
while self.at < len(self.text):
|
|
103
|
+
match self.peek():
|
|
104
|
+
case "\\":
|
|
105
|
+
self.at += 2
|
|
106
|
+
case '"':
|
|
107
|
+
self.at += 1
|
|
108
|
+
return
|
|
109
|
+
case "$":
|
|
110
|
+
self.expansion()
|
|
111
|
+
case "`":
|
|
112
|
+
self.at += 1
|
|
113
|
+
self.scan("`")
|
|
114
|
+
case _:
|
|
115
|
+
self.at += 1
|
|
116
|
+
|
|
117
|
+
def expansion(self) -> None:
|
|
118
|
+
match self.peek(1), self.peek(2):
|
|
119
|
+
case "(", "(":
|
|
120
|
+
self.skip_balanced("(", ")")
|
|
121
|
+
case "(", _:
|
|
122
|
+
self.at += 2
|
|
123
|
+
self.scan(")")
|
|
124
|
+
case "{", _:
|
|
125
|
+
self.skip_balanced("{", "}")
|
|
126
|
+
case "'", _:
|
|
127
|
+
self.at += 1
|
|
128
|
+
self.skip_ansi()
|
|
129
|
+
case _:
|
|
130
|
+
self.at += 1
|
|
131
|
+
|
|
132
|
+
def skip_balanced(self, opener: str, closer: str) -> None:
|
|
133
|
+
self.at += 1
|
|
134
|
+
depth = 0
|
|
135
|
+
while self.at < len(self.text):
|
|
136
|
+
char = self.peek()
|
|
137
|
+
self.at += 1
|
|
138
|
+
depth += (char == opener) - (char == closer)
|
|
139
|
+
if depth == 0:
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
def skip_ansi(self) -> None:
|
|
143
|
+
self.at += 1
|
|
144
|
+
while self.at < len(self.text):
|
|
145
|
+
match self.peek():
|
|
146
|
+
case "\\":
|
|
147
|
+
self.at += 2
|
|
148
|
+
case "'":
|
|
149
|
+
self.at += 1
|
|
150
|
+
return
|
|
151
|
+
case _:
|
|
152
|
+
self.at += 1
|
|
153
|
+
|
|
154
|
+
def heredoc(self) -> None:
|
|
155
|
+
self.at += 2
|
|
156
|
+
strip = self.peek() == "-"
|
|
157
|
+
self.at += strip
|
|
158
|
+
while self.peek() in (" ", "\t"):
|
|
159
|
+
self.at += 1
|
|
160
|
+
delimiter: list[str] = []
|
|
161
|
+
while (char := self.peek()) and char not in BREAKS:
|
|
162
|
+
match char:
|
|
163
|
+
case "'" | '"':
|
|
164
|
+
end = self.text.find(char, self.at + 1)
|
|
165
|
+
delimiter.append(self.text[self.at + 1 : end if end >= 0 else None])
|
|
166
|
+
self.at = end + 1 if end >= 0 else len(self.text)
|
|
167
|
+
case "\\":
|
|
168
|
+
delimiter.append(self.peek(1))
|
|
169
|
+
self.at += 2
|
|
170
|
+
case _:
|
|
171
|
+
delimiter.append(char)
|
|
172
|
+
self.at += 1
|
|
173
|
+
self.heredocs.append(("".join(delimiter), strip))
|
|
174
|
+
|
|
175
|
+
def read_heredocs(self) -> None:
|
|
176
|
+
for delimiter, strip in self.heredocs:
|
|
177
|
+
while self.at < len(self.text):
|
|
178
|
+
end = self.text.find("\n", self.at)
|
|
179
|
+
line = self.text[self.at : end if end >= 0 else None]
|
|
180
|
+
self.at = end + 1 if end >= 0 else len(self.text)
|
|
181
|
+
if (line.lstrip("\t") if strip else line) == delimiter:
|
|
182
|
+
break
|
|
183
|
+
self.heredocs.clear()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def payload(call: Call) -> str | None:
|
|
187
|
+
"""The shell text ``call`` runs as code: an ``eval``'s joined arguments, or a shell's ``-c`` command string."""
|
|
188
|
+
if call.name == "eval":
|
|
189
|
+
return " ".join(call.args) or None
|
|
190
|
+
if call.name not in SHELLS:
|
|
191
|
+
return None
|
|
192
|
+
args, inline = iter(call.args), False
|
|
193
|
+
for arg in args:
|
|
194
|
+
if arg in OPTION_VALUES:
|
|
195
|
+
next(args, None)
|
|
196
|
+
elif arg.startswith(("-", "+")) and not arg.startswith("--"):
|
|
197
|
+
inline = inline or (arg.startswith("-") and "c" in arg[1:])
|
|
198
|
+
elif not arg.startswith("--"):
|
|
199
|
+
return arg if inline else None
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def shell_comments(text: str, depth: int = 0) -> Iterator[str]:
|
|
204
|
+
"""Every real shell comment in ``text``, the code of nested ``sh -c`` and ``eval`` payloads included."""
|
|
205
|
+
(scanner := CommentScanner(text)).scan()
|
|
206
|
+
yield from scanner.found
|
|
207
|
+
if depth >= PAYLOAD_DEPTH_LIMIT or (cmd := Cmd.parse(text)) is None:
|
|
208
|
+
return
|
|
209
|
+
for call in cmd.calls():
|
|
210
|
+
if call.occurrence.host is None and (code := payload(call)) is not None:
|
|
211
|
+
yield from shell_comments(code, depth + 1)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def comment_pairs(text: str) -> Iterator[Pair]:
|
|
215
|
+
for comment in shell_comments(text):
|
|
216
|
+
for word in comment.lstrip("#").split():
|
|
217
|
+
if token := COMMENT_TOKEN.fullmatch(word):
|
|
218
|
+
yield pair(*token.groups(""))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def header_lines(text: str) -> Iterator[str]:
|
|
222
|
+
"""The lines of a dispatch prompt that speak for the prompt itself: none inside a fence or pasted content."""
|
|
223
|
+
fence, pasted = "", False
|
|
224
|
+
for line in map(str.strip, text.splitlines()):
|
|
225
|
+
if fence:
|
|
226
|
+
fence = "" if line.startswith(fence) else fence
|
|
227
|
+
elif pasted:
|
|
228
|
+
pasted = "</pasted_content>" not in line
|
|
229
|
+
elif opened := FENCE.match(line):
|
|
230
|
+
fence = opened.group(1)
|
|
231
|
+
elif line.startswith("<pasted_content"):
|
|
232
|
+
pasted = "</pasted_content>" not in line
|
|
233
|
+
else:
|
|
234
|
+
yield line
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def dispatch_pairs(text: str) -> Iterator[Pair]:
|
|
238
|
+
for line in header_lines(text):
|
|
239
|
+
if declared := DISPATCH_LINE.fullmatch(line):
|
|
240
|
+
yield from (pair(*found.groups("")) for found in DISPATCH_PAIR.finditer(declared.group(1)))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def env_pairs() -> Iterator[Pair]:
|
|
244
|
+
if (reqenv.getenv(RAW_ENV) or "").strip().lower() in TRUTHY:
|
|
245
|
+
yield "raw", None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def dispatch_text(evt: BaseHookEvent) -> str:
|
|
249
|
+
match evt.input:
|
|
250
|
+
case TaskCall(prompt=prompt):
|
|
251
|
+
return prompt or ""
|
|
252
|
+
case SkillCall(args=args):
|
|
253
|
+
return args or ""
|
|
254
|
+
return ""
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def event_annotations(evt: BaseHookEvent) -> Annotations:
|
|
258
|
+
return dict(chain(env_pairs(), comment_pairs(evt.cmd.raw), dispatch_pairs(dispatch_text(evt))))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def dispatch_prompt(evt: BaseHookEvent) -> str:
|
|
262
|
+
t = evt.ctx.t
|
|
263
|
+
if isinstance(t, RemoteSession):
|
|
264
|
+
return next(iter(t.prompts(selection="first", count=1)), "")
|
|
265
|
+
return next((turn.prompt for turn in t.turns if turn.prompt), "")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def session_annotations(evt: BaseHookEvent) -> Annotations:
|
|
269
|
+
"""The annotations the session's dispatch prompt — its first prompt, a lane's brief — declares."""
|
|
270
|
+
return dict(dispatch_pairs(dispatch_prompt(evt)))
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class Annotated(CustomCondition):
|
|
274
|
+
"""Matches when the event carries the ``ccx:`` annotation ``key``, set to ``value`` when one is given.
|
|
275
|
+
|
|
276
|
+
Three carriers feed :attr:`~captain_hook.BaseHookEvent.annotations`: a ``ccx:<key>[=<value>]`` token in a
|
|
277
|
+
real comment of a Bash command (``gt submit # ccx:raw``), a whole ``ccx: <key>[=<value>] ...`` line in an
|
|
278
|
+
Agent or Task prompt or in Skill args, and ``CAPT_HOOK_CCX_RAW=1`` for ``raw``. ``scope="session"`` reads
|
|
279
|
+
only the session's dispatch prompt instead, so a lane briefed with ``ccx: role=fix`` matches on every call and
|
|
280
|
+
a command can never claim the role for itself.
|
|
281
|
+
|
|
282
|
+
Example:
|
|
283
|
+
>>> hook(Event.PreToolUse, only_if=[Runs("gt", "submit")], skip_if=[Annotated("raw")], message="...")
|
|
284
|
+
>>> Annotated("role", "evidence", scope="session")
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
def __init__(self, key: str, value: str | None = None, *, scope: Literal["event", "session"] = "event") -> None:
|
|
288
|
+
self.key = key
|
|
289
|
+
self.value = value
|
|
290
|
+
self.scope = scope
|
|
291
|
+
|
|
292
|
+
def matches(self, annotations: Annotations) -> bool:
|
|
293
|
+
return self.key in annotations and (self.value is None or annotations[self.key] == self.value)
|
|
294
|
+
|
|
295
|
+
def check(self, evt: BaseHookEvent) -> bool:
|
|
296
|
+
return self.matches(session_annotations(evt) if self.scope == "session" else evt.annotations)
|
|
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any, get_args
|
|
|
12
12
|
from captain_hook.conditions import matches_conditions
|
|
13
13
|
from captain_hook.state import caller_file, hook_name
|
|
14
14
|
from captain_hook.types import (
|
|
15
|
+
TOOL_EVENTS,
|
|
15
16
|
CustomCondition,
|
|
16
17
|
Event,
|
|
17
18
|
HookSpec,
|
|
@@ -24,6 +25,7 @@ from captain_hook.types import (
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
25
26
|
from cc_transcript.activity import UserClassifier
|
|
26
27
|
|
|
28
|
+
from captain_hook.confirm import Confirm
|
|
27
29
|
from captain_hook.events import BaseHookEvent
|
|
28
30
|
from captain_hook.settings import HooksSettings
|
|
29
31
|
from captain_hook.types import HookResult
|
|
@@ -69,6 +71,14 @@ def reject_async_fail_closed(async_: bool, on_incomplete: str | None) -> None:
|
|
|
69
71
|
raise AsyncDecisionError("on_incomplete is invalid with async_=True: an async hook has no call to block.")
|
|
70
72
|
|
|
71
73
|
|
|
74
|
+
def reject_confirm_misuse(events: Event, block: bool, confirm: Confirm | None) -> None:
|
|
75
|
+
"""Reject a ``confirm`` with no block to confirm, or on an event with no tool call for the model to judge."""
|
|
76
|
+
if confirm is not None and not block:
|
|
77
|
+
raise ValueError("confirm needs block=True: it settles a block, and a warn has nothing to settle.")
|
|
78
|
+
if confirm is not None and events & ~TOOL_EVENTS:
|
|
79
|
+
raise ValueError("confirm judges a tool call, so it runs only on tool events.")
|
|
80
|
+
|
|
81
|
+
|
|
72
82
|
def reject_transcript_events(transcript_events: int | None) -> None:
|
|
73
83
|
"""Reject a declared window the snapshot engine's ``tail`` operation cannot serve."""
|
|
74
84
|
if transcript_events is not None and not 0 < transcript_events <= MAX_TRANSCRIPT_EVENTS:
|
|
@@ -216,6 +226,7 @@ def hook(
|
|
|
216
226
|
only_if: Sequence[TCondition] = (),
|
|
217
227
|
skip_if: Sequence[TCondition] = (),
|
|
218
228
|
block: bool = False,
|
|
229
|
+
confirm: Confirm | None = None,
|
|
219
230
|
advisory_on_deny: bool = False,
|
|
220
231
|
respect_gitignore: bool = True,
|
|
221
232
|
max_fires: int | None = None,
|
|
@@ -227,6 +238,7 @@ def hook(
|
|
|
227
238
|
) -> None:
|
|
228
239
|
reject_async_decision(events, async_)
|
|
229
240
|
reject_mandatory_misuse(events, async_, mandatory)
|
|
241
|
+
reject_confirm_misuse(events, block, confirm)
|
|
230
242
|
reject_transcript_events(transcript_events)
|
|
231
243
|
validate_conditions(only_if, "only_if", events)
|
|
232
244
|
validate_conditions(skip_if, "skip_if", events)
|
|
@@ -238,6 +250,7 @@ def hook(
|
|
|
238
250
|
skip_if=tuple(skip_if),
|
|
239
251
|
message=message,
|
|
240
252
|
block=block,
|
|
253
|
+
confirm=confirm,
|
|
241
254
|
advisory_on_deny=advisory_on_deny,
|
|
242
255
|
respect_gitignore=respect_gitignore,
|
|
243
256
|
max_fires=max_fires,
|
|
@@ -8,15 +8,13 @@ from pathlib import Path
|
|
|
8
8
|
from typing import TYPE_CHECKING, Any
|
|
9
9
|
|
|
10
10
|
from captain_hook import BaseHookEvent, CustomCommandLineCondition, CustomCondition, FileFixture, Input
|
|
11
|
-
from captain_hook.util import
|
|
12
|
-
from captain_hook.util.vcs import ccx_raw_marked, graphite_lane, graphite_lane_of_git_dir
|
|
11
|
+
from captain_hook.util.vcs import graphite_lane, graphite_lane_of_git_dir
|
|
13
12
|
|
|
14
13
|
if TYPE_CHECKING:
|
|
15
14
|
from cc_transcript.command import CommandLine
|
|
16
15
|
|
|
17
16
|
from captain_hook.cmd import Call
|
|
18
17
|
|
|
19
|
-
RAW_ENV = "CAPT_HOOK_CCX_RAW"
|
|
20
18
|
FIXTURE_GITDIR = Path(__file__).parents[1] / "gitdir"
|
|
21
19
|
|
|
22
20
|
JJ_READS = frozenset(
|
|
@@ -167,14 +165,6 @@ class CcxInstalled(CustomCondition):
|
|
|
167
165
|
return shutil.which("ccx") is not None
|
|
168
166
|
|
|
169
167
|
|
|
170
|
-
class RawRequested(CustomCommandLineCondition):
|
|
171
|
-
"""Matches a command that opts out of ccx steering: a ``# ccx:raw`` comment on the line, or
|
|
172
|
-
``CAPT_HOOK_CCX_RAW`` set for the session. Either runs the raw command as written."""
|
|
173
|
-
|
|
174
|
-
def check_command_line(self, evt: BaseHookEvent, cl: CommandLine) -> bool:
|
|
175
|
-
return bool(reqenv.getenv(RAW_ENV)) or ccx_raw_marked(evt.cmd.raw)
|
|
176
|
-
|
|
177
|
-
|
|
178
168
|
def git_probe(call: Call, session_cwd: Path | None, *args: str) -> str | None:
|
|
179
169
|
"""Run a read-only ``git`` query against the repository ``call`` targets; ``None`` when git exits non-zero."""
|
|
180
170
|
cwd, git_dir = git_location(call, session_cwd)
|
|
@@ -7,10 +7,9 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
|
|
8
8
|
from loguru import logger
|
|
9
9
|
|
|
10
|
-
from captain_hook import Allow, BaseHookEvent, Event, HookResult, Input, Tool, on
|
|
10
|
+
from captain_hook import Allow, Annotated, BaseHookEvent, Event, HookResult, Input, Tool, on
|
|
11
11
|
from captain_hook.builtin_packs.graphite.hooks._lib import (
|
|
12
12
|
CcxInstalled,
|
|
13
|
-
RawRequested,
|
|
14
13
|
git_location,
|
|
15
14
|
git_probe,
|
|
16
15
|
graphite_owns,
|
|
@@ -228,7 +227,7 @@ def holds(cwd: Path, planned: list[Push]) -> list[Push]:
|
|
|
228
227
|
@on(
|
|
229
228
|
Event.PreToolUse,
|
|
230
229
|
only_if=[Tool("Bash"), CcxInstalled()],
|
|
231
|
-
skip_if=[
|
|
230
|
+
skip_if=[Annotated("raw")],
|
|
232
231
|
tests={
|
|
233
232
|
Input(command="git push", cwd="/"): Allow(),
|
|
234
233
|
Input(command="ccx vcs push", cwd="/"): Allow(),
|
|
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
|
|
|
6
6
|
|
|
7
7
|
from captain_hook import (
|
|
8
8
|
Allow,
|
|
9
|
+
Annotated,
|
|
9
10
|
BaseHookEvent,
|
|
10
11
|
CommandSchema,
|
|
11
12
|
CustomCommandLineCondition,
|
|
@@ -30,7 +31,6 @@ from captain_hook.builtin_packs.graphite.hooks._lib import (
|
|
|
30
31
|
HasFlag,
|
|
31
32
|
JJReads,
|
|
32
33
|
PushesTagRef,
|
|
33
|
-
RawRequested,
|
|
34
34
|
force_pushes,
|
|
35
35
|
git_location,
|
|
36
36
|
git_probe,
|
|
@@ -67,7 +67,7 @@ GH_PR_VIEW = CommandSchema(
|
|
|
67
67
|
hook(
|
|
68
68
|
Event.PreToolUse,
|
|
69
69
|
only_if=[Tool("Bash"), GraphiteRuns(("jj",))],
|
|
70
|
-
skip_if=[JJReads(),
|
|
70
|
+
skip_if=[JJReads(), Annotated("raw")],
|
|
71
71
|
message=(
|
|
72
72
|
"The repository this command targets keeps its stack in Graphite, which jj writes leave stale. "
|
|
73
73
|
'Run `ccx vcs ship -m "<msg>"` to commit and submit instead.'
|
|
@@ -77,6 +77,10 @@ hook(
|
|
|
77
77
|
in_graphite_repo("jj log && jj new"): Warn(pattern="Graphite"),
|
|
78
78
|
in_graphite_repo("jj log"): Allow(),
|
|
79
79
|
in_graphite_repo("jj new # ccx:raw"): Allow(),
|
|
80
|
+
in_graphite_repo("bash -c 'jj new # ccx:raw'"): Allow(),
|
|
81
|
+
in_graphite_repo("jj new && echo '# ccx:raw'"): Warn(pattern="Graphite"),
|
|
82
|
+
in_graphite_repo("jj new", env={"CAPT_HOOK_CCX_RAW": "0"}): Warn(pattern="Graphite"),
|
|
83
|
+
in_graphite_repo("jj new", env={"CAPT_HOOK_CCX_RAW": "true"}): Allow(),
|
|
80
84
|
Input(command="jj new", cwd="/"): Allow(),
|
|
81
85
|
},
|
|
82
86
|
)
|
|
@@ -96,7 +100,7 @@ hook(
|
|
|
96
100
|
("git", "checkout", "-B"),
|
|
97
101
|
),
|
|
98
102
|
],
|
|
99
|
-
skip_if=[HasFlag("--dry-run"), HasFlag("--tags"), PushesTagRef(),
|
|
103
|
+
skip_if=[HasFlag("--dry-run"), HasFlag("--tags"), PushesTagRef(), Annotated("raw")],
|
|
100
104
|
message=(
|
|
101
105
|
"The repository this command targets keeps its stack in Graphite, so raw git commits, branches, and "
|
|
102
106
|
'pushes leave it stale. Run `ccx vcs ship -m "<msg>"` instead.'
|
|
@@ -123,11 +127,21 @@ hook(
|
|
|
123
127
|
HasFlag("--dry-run"),
|
|
124
128
|
HasFlag("--no-push"),
|
|
125
129
|
HasFlag("--help", "-h"),
|
|
130
|
+
Annotated("role", scope="session"),
|
|
126
131
|
],
|
|
127
132
|
message="Submit a change only after a review pass over its diff. Run `/cc-review:start` first.",
|
|
128
133
|
tests={
|
|
129
134
|
in_graphite_repo("gt submit"): Warn(pattern="review pass"),
|
|
130
135
|
in_graphite_repo("ccx vcs ship -m x"): Warn(pattern="review pass"),
|
|
136
|
+
in_graphite_repo(
|
|
137
|
+
'ccx vcs ship -m "fix: x" --new-branch=hooks/x', transcript=[T.user("ccx: role=fix\nShip the fix.")]
|
|
138
|
+
): Allow(),
|
|
139
|
+
in_graphite_repo(
|
|
140
|
+
'ccx vcs ship -m "fix: x" --new-branch=hooks/x', transcript=[T.user("Give this lane role=fix and ship.")]
|
|
141
|
+
): Warn(pattern="review pass"),
|
|
142
|
+
in_graphite_repo("gt submit # ccx:role=fix", transcript=[T.user("Ship this change.")]): Warn(
|
|
143
|
+
pattern="review pass"
|
|
144
|
+
),
|
|
131
145
|
in_graphite_repo(
|
|
132
146
|
"gt submit", transcript=[T.assistant(T.tool("Skill", skill="cc-review:start")), T.user("ship it")]
|
|
133
147
|
): Allow(),
|
|
@@ -175,7 +189,7 @@ hook(
|
|
|
175
189
|
Tool("Bash"),
|
|
176
190
|
GraphiteRuns(("git", "rebase"), ("git", "merge"), ("git", "pull")),
|
|
177
191
|
],
|
|
178
|
-
skip_if=[HasFlag("--abort", "--continue", "--quit"),
|
|
192
|
+
skip_if=[HasFlag("--abort", "--continue", "--quit"), Annotated("raw")],
|
|
179
193
|
message=(
|
|
180
194
|
"A raw `git rebase`, `merge`, or `pull` leaves Graphite's parent records stale. "
|
|
181
195
|
"Run `ccx vcs stack rebase` to replay the stack instead."
|
|
@@ -303,7 +317,7 @@ def hand_run_force_push(call: Call, evt: BaseHookEvent) -> bool:
|
|
|
303
317
|
@on(
|
|
304
318
|
Event.PreToolUse,
|
|
305
319
|
only_if=[Tool("Bash"), CcxInstalled()],
|
|
306
|
-
skip_if=[
|
|
320
|
+
skip_if=[Annotated("raw")],
|
|
307
321
|
tests={
|
|
308
322
|
Input(command="gt submit", cwd="/"): Allow(),
|
|
309
323
|
Input(command="gt restack", cwd="/"): Allow(),
|
|
@@ -337,7 +351,7 @@ def stack_writes_go_through_ccx(evt: BaseHookEvent) -> HookResult | None:
|
|
|
337
351
|
@on(
|
|
338
352
|
Event.PreToolUse,
|
|
339
353
|
only_if=[Tool("Bash"), CcxInstalled(), GraphiteCall(hand_run_submit)],
|
|
340
|
-
skip_if=[
|
|
354
|
+
skip_if=[Annotated("raw")],
|
|
341
355
|
tests={Input(command="gt submit --ai", cwd="/"): Allow(), Input(command="git status", cwd="/"): Allow()},
|
|
342
356
|
)
|
|
343
357
|
def gt_submit_through_ccx(evt: BaseHookEvent) -> HookResult | None:
|
|
@@ -349,7 +363,7 @@ def gt_submit_through_ccx(evt: BaseHookEvent) -> HookResult | None:
|
|
|
349
363
|
@on(
|
|
350
364
|
Event.PreToolUse,
|
|
351
365
|
only_if=[Tool("Bash"), CcxInstalled(), GraphiteCall(hand_run_restack)],
|
|
352
|
-
skip_if=[
|
|
366
|
+
skip_if=[Annotated("raw")],
|
|
353
367
|
tests={Input(command="gt sync", cwd="/"): Allow(), Input(command="gt log", cwd="/"): Allow()},
|
|
354
368
|
)
|
|
355
369
|
def gt_restack_through_ccx(evt: BaseHookEvent) -> HookResult | None:
|
|
@@ -362,7 +376,7 @@ def gt_restack_through_ccx(evt: BaseHookEvent) -> HookResult | None:
|
|
|
362
376
|
@on(
|
|
363
377
|
Event.PreToolUse,
|
|
364
378
|
only_if=[Tool("Bash"), CcxInstalled(), GraphiteCall(hand_run_create)],
|
|
365
|
-
skip_if=[
|
|
379
|
+
skip_if=[Annotated("raw")],
|
|
366
380
|
tests={Input(command="gt create feat -m x", cwd="/"): Allow(), Input(command="gt log", cwd="/"): Allow()},
|
|
367
381
|
)
|
|
368
382
|
def gt_create_through_ccx(evt: BaseHookEvent) -> HookResult | None:
|
|
@@ -375,7 +389,7 @@ def gt_create_through_ccx(evt: BaseHookEvent) -> HookResult | None:
|
|
|
375
389
|
@on(
|
|
376
390
|
Event.PreToolUse,
|
|
377
391
|
only_if=[Tool("Bash"), CcxInstalled(), GraphiteCall(hand_run_modify)],
|
|
378
|
-
skip_if=[
|
|
392
|
+
skip_if=[Annotated("raw")],
|
|
379
393
|
tests={Input(command="gt modify -m x", cwd="/"): Allow(), Input(command="gt log", cwd="/"): Allow()},
|
|
380
394
|
)
|
|
381
395
|
def gt_modify_through_ccx(evt: BaseHookEvent) -> HookResult | None:
|
|
@@ -387,7 +401,7 @@ def gt_modify_through_ccx(evt: BaseHookEvent) -> HookResult | None:
|
|
|
387
401
|
@on(
|
|
388
402
|
Event.PreToolUse,
|
|
389
403
|
only_if=[Tool("Bash"), CcxInstalled(), GraphiteCall(hand_run_rebase)],
|
|
390
|
-
skip_if=[
|
|
404
|
+
skip_if=[Annotated("raw")],
|
|
391
405
|
tests={Input(command="git rebase main", cwd="/"): Allow(), Input(command="git rebase --abort", cwd="/"): Allow()},
|
|
392
406
|
)
|
|
393
407
|
def git_rebase_through_ccx(evt: BaseHookEvent) -> HookResult | None:
|
|
@@ -400,7 +414,7 @@ def git_rebase_through_ccx(evt: BaseHookEvent) -> HookResult | None:
|
|
|
400
414
|
@on(
|
|
401
415
|
Event.PreToolUse,
|
|
402
416
|
only_if=[Tool("Bash"), CcxInstalled(), GraphiteCall(conflict_rebase_control)],
|
|
403
|
-
skip_if=[
|
|
417
|
+
skip_if=[Annotated("raw")],
|
|
404
418
|
tests={
|
|
405
419
|
Input(command="git rebase --continue", cwd="/"): Allow(),
|
|
406
420
|
Input(command="git rebase main", cwd="/"): Allow(),
|
|
@@ -415,7 +429,7 @@ def conflict_workspace_finishes_through_ccx(evt: BaseHookEvent) -> HookResult |
|
|
|
415
429
|
@on(
|
|
416
430
|
Event.PreToolUse,
|
|
417
431
|
only_if=[Tool("Bash"), CcxInstalled(), GraphiteCall(hand_run_force_push)],
|
|
418
|
-
skip_if=[
|
|
432
|
+
skip_if=[Annotated("raw")],
|
|
419
433
|
tests={Input(command="git push -f origin feat", cwd="/"): Allow(), Input(command="git push", cwd="/"): Allow()},
|
|
420
434
|
)
|
|
421
435
|
def force_push_through_ccx(evt: BaseHookEvent) -> HookResult | None:
|