capt-hook 10.2.0__tar.gz → 10.4.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-10.2.0 → capt_hook-10.4.0}/PKG-INFO +1 -1
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/.claude-plugin/plugin.json +1 -1
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/ast_grep.py +71 -52
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/cli.py +2 -0
- capt_hook-10.4.0/captain_hook/helper/__init__.py +8 -0
- capt_hook-10.4.0/captain_hook/helper/cli.py +59 -0
- capt_hook-10.4.0/captain_hook/helper/client.py +201 -0
- capt_hook-10.4.0/captain_hook/packs/general/capt-hook.toml +6 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/comments.py +236 -26
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/cli.py +50 -9
- capt_hook-10.4.0/captain_hook/review/notify.py +116 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/pipeline.py +10 -3
- capt_hook-10.4.0/captain_hook/review/snapshot.py +137 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/store.py +50 -16
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +6 -8
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +11 -7
- {capt_hook-10.2.0 → capt_hook-10.4.0}/hatch_build.py +94 -4
- {capt_hook-10.2.0 → capt_hook-10.4.0}/pyproject.toml +1 -1
- capt_hook-10.2.0/captain_hook/packs/general/capt-hook.toml +0 -6
- {capt_hook-10.2.0 → capt_hook-10.4.0}/.gitignore +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/LICENSE +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/README.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/capt_hook_client/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/capt_hook_client/__main__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/capt_hook_client/client.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/capt_hook_client/key.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/__init__.pyi +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/__main__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/app.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/classifiers/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/classifiers/conductor.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/conditions.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/context.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/contexts.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/context.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/decision_writer.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/lifecycle.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/logsink.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/ops.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/protocol.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/registry.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/server.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/daemon/transcache.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/decisions.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/dispatch.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/durable.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/events.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/fields.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/file.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/heartbeat.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/hooks/hooks.json +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/loader.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/log.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/bootstrap.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/contract.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/fixes/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/fixes/_lib.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/fixes/capt-hook.toml +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/fixes/scratch_writes.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/fixes/teammate_permissions.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/_lib.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/commands.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/deletions.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/detours.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/docs.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/models.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/plans.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/fragments/deliverable_rubric.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/fragments/workflow_script_header.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/browser_delegation_nudge.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/implementation_spawn_nudge.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/inline_edit_nudge.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/prose_spawn_gate.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/prose_workflow_nudge.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/review_routing_spawn_nudge.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/review_routing_workflow_nudge.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/writing_docs_spawn_nudge.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/models/writing_docs_workflow_nudge.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts/tools/preload_nudge.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/prompts.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/review.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/tasks.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/tombstones.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/general/tools.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/go/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/go/capt-hook.toml +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/go/testing.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/go/toolchain.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/manager.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/performance/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/performance/capt-hook.toml +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/performance/pipelining.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/plugins.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/python/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/python/capt-hook.toml +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/python/style.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/python/testing.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/python/toolchain.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/scaffold.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/steering/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/steering/capt-hook.toml +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/steering/steering.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/packs/steering/teammates.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/primitives/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/primitives/commands.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/primitives/llm.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/primitives/permissions.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/primitives/rewrite.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/primitives/workflow.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/prompt.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/py.typed +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/announce.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/dashboard.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/fix.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/formats.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/judge.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/prompts.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/repo.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/routing.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/scan.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/settings.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/status.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/sync.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/review/triage.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/session.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/settings.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/signals/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/signals/nlp.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/state.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/style/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/style/ast_grep.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/style/matchers.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/style/scope.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/style/types.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/tasks.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/templates/pack_hook.py.tmpl +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/testing/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/testing/helpers.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/testing/types.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/transcripts.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/types.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/__init__.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/automode.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/caching.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/fs.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/http.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/model_cache.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/paths.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/proc.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/reqenv.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/scratch.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/shell.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/text.py +0 -0
- {capt_hook-10.2.0 → capt_hook-10.4.0}/captain_hook/util/vcs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "captain-hook",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.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": {
|
|
@@ -18,9 +18,11 @@ import re
|
|
|
18
18
|
from collections import Counter
|
|
19
19
|
from collections.abc import Iterable, Iterator, Set
|
|
20
20
|
from dataclasses import dataclass
|
|
21
|
-
from typing import TYPE_CHECKING
|
|
21
|
+
from typing import TYPE_CHECKING, NamedTuple
|
|
22
22
|
|
|
23
23
|
from captain_hook.langs import COMMENT_TYPES as GENERATED_COMMENT_TYPES
|
|
24
|
+
from captain_hook.langs import DOC_COMMENT_KINDS as GENERATED_DOC_COMMENT_KINDS
|
|
25
|
+
from captain_hook.langs import DOC_SIBLINGS as GENERATED_DOC_SIBLINGS
|
|
24
26
|
from captain_hook.langs import LANG_GLOBS
|
|
25
27
|
|
|
26
28
|
if TYPE_CHECKING:
|
|
@@ -46,39 +48,11 @@ MAX_COMMENT_LINES = 3
|
|
|
46
48
|
MAX_COMMENT_CHARS = 200
|
|
47
49
|
MAX_COMMENT_SCAN_BYTES = 512_000
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"tsx": ("/**",),
|
|
55
|
-
"java": ("/**",),
|
|
56
|
-
"kotlin": ("/**",),
|
|
57
|
-
"swift": ("///", "/**"),
|
|
58
|
-
"dart": ("///",),
|
|
59
|
-
"cs": ("///",),
|
|
60
|
-
"php": ("/**",),
|
|
61
|
-
"scala": ("/**",),
|
|
62
|
-
}
|
|
63
|
-
"""Comment prefixes that mark a documentation comment, by language."""
|
|
64
|
-
|
|
65
|
-
GO_DOC_SIBLINGS: frozenset[str] = frozenset(
|
|
66
|
-
{
|
|
67
|
-
"function_declaration",
|
|
68
|
-
"method_declaration",
|
|
69
|
-
"type_declaration",
|
|
70
|
-
"var_declaration",
|
|
71
|
-
"const_declaration",
|
|
72
|
-
"package_clause",
|
|
73
|
-
"const_spec",
|
|
74
|
-
"var_spec",
|
|
75
|
-
"field_declaration",
|
|
76
|
-
"import_declaration",
|
|
77
|
-
"method_elem",
|
|
78
|
-
}
|
|
79
|
-
)
|
|
80
|
-
"""Kinds a Go run must immediately precede to read as godoc — top-level declarations plus grouped
|
|
81
|
-
``const``/``var`` specs, struct fields, imports, and interface methods (which nest below the file)."""
|
|
51
|
+
DOC_COMMENT_KINDS: frozenset[str] = GENERATED_DOC_COMMENT_KINDS
|
|
52
|
+
"""Grammar-defined kinds that natively mark a documentation comment."""
|
|
53
|
+
|
|
54
|
+
DOC_SIBLINGS: dict[str, frozenset[str]] = GENERATED_DOC_SIBLINGS
|
|
55
|
+
"""Concrete declaration kinds a comment run may document, by language."""
|
|
82
56
|
|
|
83
57
|
|
|
84
58
|
@dataclass(frozen=True, slots=True)
|
|
@@ -179,6 +153,16 @@ def introduced_comments(old: str, new: str, lang: str) -> Iterator[Match]:
|
|
|
179
153
|
return introduced(comments(old, lang), comments(new, lang))
|
|
180
154
|
|
|
181
155
|
|
|
156
|
+
class CommentSegment(NamedTuple):
|
|
157
|
+
"""Line and character sizes of one alphanumeric-bearing portion of a comment block."""
|
|
158
|
+
|
|
159
|
+
lines: int
|
|
160
|
+
chars: int
|
|
161
|
+
|
|
162
|
+
def over(self, max_lines: int, max_chars: int) -> bool:
|
|
163
|
+
return self.lines > max_lines or self.chars > max_chars
|
|
164
|
+
|
|
165
|
+
|
|
182
166
|
@dataclass(frozen=True, slots=True)
|
|
183
167
|
class CommentRun:
|
|
184
168
|
"""A maximal run of adjacent line-leading comments (or one block comment), located by 1-based
|
|
@@ -209,7 +193,8 @@ class CommentRun:
|
|
|
209
193
|
class CommentBlock:
|
|
210
194
|
"""One or more comment runs a size check treats as a unit: adjacent line-leading runs and the
|
|
211
195
|
paragraphs they form across blank-only gaps. ``lines``/``chars`` sum the constituent runs (the
|
|
212
|
-
blank gaps don't count),
|
|
196
|
+
blank gaps don't count), the block is doc only when every run is, and doc budgets split its
|
|
197
|
+
opening paragraph from the combined trailing paragraphs without changing the runs."""
|
|
213
198
|
|
|
214
199
|
runs: tuple[CommentRun, ...]
|
|
215
200
|
|
|
@@ -237,9 +222,40 @@ class CommentBlock:
|
|
|
237
222
|
def key(self) -> str:
|
|
238
223
|
return " ".join("\n".join(t for run in self.runs for t in run.texts).split())
|
|
239
224
|
|
|
225
|
+
@property
|
|
226
|
+
def doc_paragraphs(self) -> tuple[CommentSegment, CommentSegment]:
|
|
227
|
+
"""Opening and trailing doc portions, split at the first alphanumeric-free row.
|
|
228
|
+
|
|
229
|
+
A bare comment leader, gutter, divider, or source gap between runs is a separator. Decoration
|
|
230
|
+
and separator rows count toward neither portion.
|
|
231
|
+
"""
|
|
232
|
+
rows = [row for run in self.runs for text in ("", *run.texts) for row in text.rstrip("\n").split("\n")]
|
|
233
|
+
head: list[str] = []
|
|
234
|
+
tail: list[str] = []
|
|
235
|
+
head_chars = 0
|
|
236
|
+
tail_chars = 0
|
|
237
|
+
parted = False
|
|
238
|
+
for row in rows:
|
|
239
|
+
content = any(c.isalnum() for c in row)
|
|
240
|
+
if not content:
|
|
241
|
+
parted = parted or bool(head)
|
|
242
|
+
if parted:
|
|
243
|
+
tail_chars += len(row)
|
|
244
|
+
if content:
|
|
245
|
+
tail.append(row)
|
|
246
|
+
else:
|
|
247
|
+
head_chars += len(row)
|
|
248
|
+
if content:
|
|
249
|
+
head.append(row)
|
|
250
|
+
return CommentSegment(len(head), head_chars), CommentSegment(len(tail), tail_chars)
|
|
251
|
+
|
|
240
252
|
@property
|
|
241
253
|
def too_long(self) -> bool:
|
|
242
|
-
|
|
254
|
+
"""Whether the block exceeds its plain or documentation-comment budget."""
|
|
255
|
+
if not self.doc:
|
|
256
|
+
return self.lines > MAX_COMMENT_LINES or self.chars > MAX_COMMENT_CHARS
|
|
257
|
+
head, tail = self.doc_paragraphs
|
|
258
|
+
return head.over(6, 400) or tail.over(MAX_COMMENT_LINES, MAX_COMMENT_CHARS)
|
|
243
259
|
|
|
244
260
|
|
|
245
261
|
def line_span(node: SyntaxNode) -> tuple[int, int]:
|
|
@@ -262,19 +278,19 @@ def is_line_leading(node: SyntaxNode, src_lines: list[str]) -> bool:
|
|
|
262
278
|
|
|
263
279
|
|
|
264
280
|
def is_doc_run(nodes: list[SyntaxNode], lang: str) -> bool:
|
|
265
|
-
"""Whether a comment run is a documentation comment
|
|
281
|
+
"""Whether a comment run is a documentation comment.
|
|
266
282
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
interface method — begins on the immediately following line (a blank line breaks the bond).
|
|
283
|
+
Native markers require every node to carry a grammar-defined doc kind, so narrative cannot ride
|
|
284
|
+
one marked line. Otherwise, a run is documentation when its next sibling is a declaration that
|
|
285
|
+
begins on the immediately following line; a blank line breaks the bond.
|
|
271
286
|
"""
|
|
272
|
-
if
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
287
|
+
if all(
|
|
288
|
+
node.kind in DOC_COMMENT_KINDS or any(child.kind() in DOC_COMMENT_KINDS for child in node.raw.children())
|
|
289
|
+
for node in nodes
|
|
290
|
+
):
|
|
291
|
+
return True
|
|
276
292
|
last = nodes[-1].raw
|
|
277
|
-
if (nxt := last.next()) is None or nxt.kind() not in
|
|
293
|
+
if (nxt := last.next()) is None or nxt.kind() not in DOC_SIBLINGS[lang]:
|
|
278
294
|
return False
|
|
279
295
|
return nxt.range().start.line == last.range().end.line + 1
|
|
280
296
|
|
|
@@ -343,19 +359,22 @@ def touched_comment_blocks(old: str, new: str, lang: str) -> list[CommentBlock]:
|
|
|
343
359
|
"""Comment blocks of ``new`` this edit created or grew — a multiset diff over :attr:`CommentBlock.key`.
|
|
344
360
|
|
|
345
361
|
An old block exempts one identically-keyed new block (a genuine move stays exempt; a duplicated
|
|
346
|
-
copy past the first counts as touched).
|
|
347
|
-
with the same key was *also*
|
|
348
|
-
pushes a previously-fine run over
|
|
362
|
+
copy past the first counts as touched). An over-budget new block is exempt only when an old block
|
|
363
|
+
with the same key was *also* over budget, so a legacy oversized run stays quiet while a reflow
|
|
364
|
+
that pushes a previously-fine run over its budget does not.
|
|
349
365
|
"""
|
|
350
366
|
old_blocks = comment_blocks(old, lang)
|
|
351
367
|
remaining = Counter(block.key for block in old_blocks)
|
|
352
|
-
old_too_long =
|
|
368
|
+
old_too_long = Counter(block.key for block in old_blocks if block.too_long)
|
|
353
369
|
touched: list[CommentBlock] = []
|
|
354
370
|
for block in comment_blocks(new, lang):
|
|
355
371
|
if remaining[block.key] > 0:
|
|
356
372
|
remaining[block.key] -= 1
|
|
357
|
-
if block.too_long
|
|
358
|
-
|
|
373
|
+
if block.too_long:
|
|
374
|
+
if old_too_long[block.key] > 0:
|
|
375
|
+
old_too_long[block.key] -= 1
|
|
376
|
+
else:
|
|
377
|
+
touched.append(block)
|
|
359
378
|
else:
|
|
360
379
|
touched.append(block)
|
|
361
380
|
return touched
|
|
@@ -18,6 +18,7 @@ from loguru import logger
|
|
|
18
18
|
|
|
19
19
|
from captain_hook.app import AsyncDecisionError, _state, load_gitignore, reset
|
|
20
20
|
from captain_hook.dispatch import dispatch
|
|
21
|
+
from captain_hook.helper.cli import helper
|
|
21
22
|
from captain_hook.loader import (
|
|
22
23
|
CONF_MODULE,
|
|
23
24
|
discover_hooks,
|
|
@@ -875,6 +876,7 @@ def pack_update(state: CliState, name: str | None) -> None:
|
|
|
875
876
|
|
|
876
877
|
|
|
877
878
|
cli.add_command(review)
|
|
879
|
+
cli.add_command(helper)
|
|
878
880
|
|
|
879
881
|
|
|
880
882
|
@cli.group(hidden=True)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""The Captain Hook desktop helper's Python seam: paths, the socket client, and the CLI.
|
|
2
|
+
|
|
3
|
+
The helper itself is a signed macOS app; this package is the ``capt-hook`` side that
|
|
4
|
+
addresses it — the frozen ``helper.sock`` protocol client (:mod:`captain_hook.helper.client`)
|
|
5
|
+
and the ``capt-hook helper`` command group (:mod:`captain_hook.helper.cli`).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""The ``capt-hook helper`` command group — install, ping, and test the desktop helper.
|
|
2
|
+
|
|
3
|
+
These are the user-facing surface for ``Captain Hook.app``: ``install`` installs the cask and
|
|
4
|
+
launches it, ``status`` pings the running helper, and ``notify`` fires a test banner through
|
|
5
|
+
:mod:`captain_hook.helper.client`. Every side effect (brew, ``open``, the socket) runs only
|
|
6
|
+
when the command is invoked, never at import.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from captain_hook.helper import client
|
|
14
|
+
|
|
15
|
+
CASK = "yasyf/tap/captain-hook"
|
|
16
|
+
APP_PATH = "/Applications/Captain Hook.app"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.group()
|
|
20
|
+
def helper() -> None:
|
|
21
|
+
"""Manage the Captain Hook desktop notification helper."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@helper.command()
|
|
25
|
+
def install() -> None:
|
|
26
|
+
"""Install or upgrade the signed helper via Homebrew, then launch it in the background."""
|
|
27
|
+
import subprocess
|
|
28
|
+
|
|
29
|
+
subprocess.run(["brew", "install", "--cask", "--force", CASK], check=True)
|
|
30
|
+
if subprocess.run(["open", "-g", APP_PATH], capture_output=True).returncode != 0:
|
|
31
|
+
subprocess.run(["open", "-g", "-a", client.APP_NAME], check=False)
|
|
32
|
+
click.echo(
|
|
33
|
+
"Captain Hook installed. Grant notification permission under "
|
|
34
|
+
"System Settings > Notifications > Captain Hook, then add the review widget via "
|
|
35
|
+
"Edit Widgets in Notification Center."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@helper.command()
|
|
40
|
+
def status() -> None:
|
|
41
|
+
"""Ping the running helper and print its reported version."""
|
|
42
|
+
try:
|
|
43
|
+
reply = client.send({"v": client.PROTOCOL, "op": "ping"})
|
|
44
|
+
except (OSError, ValueError) as exc:
|
|
45
|
+
raise click.ClickException(f"helper not reachable: {exc}") from exc
|
|
46
|
+
click.echo(f"helper v{reply.get('version')} ok={reply.get('ok')}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@helper.command()
|
|
50
|
+
@click.option("--kind", default="pr_open", help="Notification kind (pr_open/pr_merged/review_failure)")
|
|
51
|
+
@click.option("--title", default="Captain Hook test", help="Banner title")
|
|
52
|
+
@click.option("--subtitle", default=None, help="Banner subtitle")
|
|
53
|
+
@click.option("--body", default="This is a test notification", help="Banner body")
|
|
54
|
+
@click.option("--url", default=None, help="Click-through URL")
|
|
55
|
+
@click.option("--repo", default=None, help="Repo key threading related notifications")
|
|
56
|
+
def notify(kind: str, title: str, subtitle: str | None, body: str | None, url: str | None, repo: str | None) -> None:
|
|
57
|
+
"""Send a test notification through the helper, launching it if needed."""
|
|
58
|
+
outcome = client.notify(kind=kind, title=title, subtitle=subtitle, body=body, url=url, repo=repo)
|
|
59
|
+
click.echo(f"lane={outcome.lane} ok={outcome.ok} error={outcome.error}")
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""The frozen ``~/.capt-hook/helper.sock`` protocol client — the single notify seam.
|
|
2
|
+
|
|
3
|
+
The desktop helper (``Captain Hook.app``) serves a newline-delimited JSON request/response
|
|
4
|
+
socket, one request per connection, LF-terminated, protocol version ``v`` 1 — the framing
|
|
5
|
+
conventions of :mod:`captain_hook.daemon.protocol`. This module is the ``capt-hook`` client:
|
|
6
|
+
it encodes a request (:func:`send`), and :func:`notify` drives the full connect → launch →
|
|
7
|
+
poll loop the review pipeline fires a notification through, modeled on
|
|
8
|
+
:mod:`capt_hook_client.client`'s ``exchange``/connect-then-spawn shape.
|
|
9
|
+
|
|
10
|
+
The paths module lives here too — :func:`helper_dir`, :func:`socket_path`, :func:`status_path`
|
|
11
|
+
are what every other helper-facing module imports. ``CAPT_HOOK_HELPER_DIR`` overrides the
|
|
12
|
+
location for tests only; the sandboxed widget appex always reads the literal ``~/.capt-hook``.
|
|
13
|
+
|
|
14
|
+
:func:`notify` never raises into its caller: it catches only ``OSError`` (connect/timeout),
|
|
15
|
+
``subprocess.SubprocessError`` (the ``open`` launch), and ``json.JSONDecodeError`` (a garbled
|
|
16
|
+
reply), and returns a typed :class:`NotifyOutcome` — a dropped notification is logged, never
|
|
17
|
+
an exception on the review hot path. There is no ``osascript`` fallback: the only lanes are
|
|
18
|
+
the socket and a logged drop.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import socket
|
|
25
|
+
import subprocess
|
|
26
|
+
import time
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from enum import StrEnum
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import TYPE_CHECKING
|
|
31
|
+
|
|
32
|
+
from loguru import logger
|
|
33
|
+
|
|
34
|
+
from captain_hook.util import reqenv
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from collections.abc import Mapping
|
|
38
|
+
|
|
39
|
+
PROTOCOL = 1
|
|
40
|
+
APP_NAME = "Captain Hook"
|
|
41
|
+
|
|
42
|
+
SOCKET_TIMEOUT = 2.0
|
|
43
|
+
OPEN_TIMEOUT = 5.0
|
|
44
|
+
LAUNCH_POLL_INTERVAL = 0.25
|
|
45
|
+
LAUNCH_POLL_BUDGET = 3.0
|
|
46
|
+
RECV_CHUNK = 65536
|
|
47
|
+
LINE_CAP = 65536
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Lane(StrEnum):
|
|
51
|
+
"""Which lane delivered (or dropped) a notification.
|
|
52
|
+
|
|
53
|
+
``socket`` — the helper accepted the request over ``helper.sock``. ``dropped`` — the helper
|
|
54
|
+
was unreachable and could not be launched (or kept refusing), so the notification was logged
|
|
55
|
+
and discarded. The draft's ``osascript`` lane is removed: an unreachable helper drops.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
socket = "socket"
|
|
59
|
+
dropped = "dropped"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, slots=True)
|
|
63
|
+
class NotifyOutcome:
|
|
64
|
+
"""The typed result of a :func:`notify` — which lane ran, whether it succeeded, and why not.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
lane: The lane that handled the request (:attr:`Lane.socket` or :attr:`Lane.dropped`).
|
|
68
|
+
ok: Whether the helper acknowledged the notification (``ok:true`` on the wire).
|
|
69
|
+
error: The failure reason — the helper's ``error`` field, or a local drop reason — else ``None``.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
lane: Lane
|
|
73
|
+
ok: bool
|
|
74
|
+
error: str | None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def helper_dir() -> Path:
|
|
78
|
+
"""The helper's home — ``~/.capt-hook`` unless ``CAPT_HOOK_HELPER_DIR`` overrides it (tests only)."""
|
|
79
|
+
if override := reqenv.getenv("CAPT_HOOK_HELPER_DIR"):
|
|
80
|
+
return Path(override)
|
|
81
|
+
return Path.home() / ".capt-hook"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def socket_path() -> Path:
|
|
85
|
+
"""The helper control socket, ``<helper_dir>/helper.sock``."""
|
|
86
|
+
return helper_dir() / "helper.sock"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def status_path() -> Path:
|
|
90
|
+
"""The snapshot the widget reads, ``<helper_dir>/status.json``."""
|
|
91
|
+
return helper_dir() / "status.json"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def encode(request: Mapping[str, object]) -> bytes:
|
|
95
|
+
"""Encodes one request to its exact wire bytes: compact JSON, declared key order, LF-terminated."""
|
|
96
|
+
return (json.dumps(request, separators=(",", ":"), ensure_ascii=False) + "\n").encode()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def send(request: Mapping[str, object], *, timeout: float = SOCKET_TIMEOUT) -> dict[str, object]:
|
|
100
|
+
"""Sends one request to the helper and returns its decoded reply — one connection, one exchange.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
OSError: The socket is unreachable, or the deadline expired mid-exchange.
|
|
104
|
+
json.JSONDecodeError: The helper's reply was not valid JSON.
|
|
105
|
+
"""
|
|
106
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
107
|
+
sock.settimeout(timeout)
|
|
108
|
+
try:
|
|
109
|
+
sock.connect(str(socket_path()))
|
|
110
|
+
sock.sendall(encode(request))
|
|
111
|
+
line = _read_line(sock)
|
|
112
|
+
finally:
|
|
113
|
+
sock.close()
|
|
114
|
+
return json.loads(line)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _read_line(sock: socket.socket) -> bytes:
|
|
118
|
+
buf = bytearray()
|
|
119
|
+
while b"\n" not in buf:
|
|
120
|
+
if not (chunk := sock.recv(RECV_CHUNK)):
|
|
121
|
+
raise OSError("helper closed the connection before responding")
|
|
122
|
+
buf.extend(chunk)
|
|
123
|
+
if len(buf) > LINE_CAP:
|
|
124
|
+
raise OSError("helper response exceeded the line cap")
|
|
125
|
+
return bytes(buf)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def notify(
|
|
129
|
+
*,
|
|
130
|
+
title: str,
|
|
131
|
+
kind: str,
|
|
132
|
+
subtitle: str | None = None,
|
|
133
|
+
body: str | None = None,
|
|
134
|
+
url: str | None = None,
|
|
135
|
+
repo: str | None = None,
|
|
136
|
+
) -> NotifyOutcome:
|
|
137
|
+
"""Fires one notification through the helper, launching it if needed — never raises.
|
|
138
|
+
|
|
139
|
+
Tries the socket; on failure launches ``Captain Hook.app`` with ``open -g`` and polls the
|
|
140
|
+
socket for up to :data:`LAUNCH_POLL_BUDGET` seconds; a helper that stays unreachable (or was
|
|
141
|
+
never installed, so ``open`` fails fast) drops with a logged warning.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
title: The banner title (required).
|
|
145
|
+
kind: The notification kind (``pr_open``/``pr_merged``/``review_failure``).
|
|
146
|
+
subtitle: The banner subtitle, omitted from the wire when ``None``.
|
|
147
|
+
body: The banner body, omitted when ``None``.
|
|
148
|
+
url: The click-through URL (http/https), omitted when ``None``.
|
|
149
|
+
repo: The repo key threading related notifications, omitted when ``None``.
|
|
150
|
+
"""
|
|
151
|
+
request: dict[str, object] = {"v": PROTOCOL, "op": "notify", "kind": kind, "title": title}
|
|
152
|
+
for field, value in (("subtitle", subtitle), ("body", body), ("url", url), ("repo", repo)):
|
|
153
|
+
if value is not None:
|
|
154
|
+
request[field] = value
|
|
155
|
+
if len(encode(request)) > LINE_CAP:
|
|
156
|
+
logger.warning("capt-hook notify request exceeds the line cap — dropped", kind=kind)
|
|
157
|
+
return NotifyOutcome(Lane.dropped, ok=False, error="request exceeds line cap")
|
|
158
|
+
|
|
159
|
+
if (outcome := _try_socket(request)) is not None:
|
|
160
|
+
return outcome
|
|
161
|
+
if not _launch():
|
|
162
|
+
logger.info("capt-hook helper not reachable and not launchable — notification dropped", kind=kind)
|
|
163
|
+
return NotifyOutcome(Lane.dropped, ok=False, error="helper not installed")
|
|
164
|
+
deadline = time.monotonic() + LAUNCH_POLL_BUDGET
|
|
165
|
+
while time.monotonic() < deadline:
|
|
166
|
+
time.sleep(LAUNCH_POLL_INTERVAL)
|
|
167
|
+
if (outcome := _try_socket(request)) is not None:
|
|
168
|
+
return outcome
|
|
169
|
+
logger.warning("capt-hook helper did not answer after launch — notification dropped", kind=kind)
|
|
170
|
+
return NotifyOutcome(Lane.dropped, ok=False, error="helper unreachable after launch")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _try_socket(request: Mapping[str, object]) -> NotifyOutcome | None:
|
|
174
|
+
"""Returns the socket-lane outcome, or ``None`` when the helper is unreachable (relaunch worth trying)."""
|
|
175
|
+
try:
|
|
176
|
+
reply = send(request)
|
|
177
|
+
except (OSError, json.JSONDecodeError):
|
|
178
|
+
return None
|
|
179
|
+
if not isinstance(reply, dict):
|
|
180
|
+
return NotifyOutcome(Lane.socket, ok=False, error=f"malformed reply: {reply!r}")
|
|
181
|
+
if reply.get("v") != PROTOCOL:
|
|
182
|
+
return NotifyOutcome(Lane.socket, ok=False, error=f"protocol mismatch: helper v={reply.get('v')!r}")
|
|
183
|
+
return NotifyOutcome(Lane.socket, ok=bool(reply.get("ok")), error=_reply_error(reply))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _reply_error(reply: Mapping[str, object]) -> str | None:
|
|
187
|
+
return str(err) if (err := reply.get("error")) is not None else None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _launch() -> bool:
|
|
191
|
+
"""Launches the helper in the background; returns ``False`` when ``open`` fails (helper not installed)."""
|
|
192
|
+
try:
|
|
193
|
+
subprocess.run(
|
|
194
|
+
["open", "-g", "-a", APP_NAME],
|
|
195
|
+
check=True,
|
|
196
|
+
capture_output=True,
|
|
197
|
+
timeout=OPEN_TIMEOUT,
|
|
198
|
+
)
|
|
199
|
+
except (OSError, subprocess.SubprocessError):
|
|
200
|
+
return False
|
|
201
|
+
return True
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
[pack]
|
|
2
|
+
name = "general"
|
|
3
|
+
version = "0.28.0"
|
|
4
|
+
description = "Language-agnostic guards: git/jj command safety, doc & prompt nudges, task & plan discipline, model-routing nudges (review, security audit, diagnosis, and bounded decision-light implementation route to gpt-5.6-sol via the codex-wrapper agent; ambiguous/decision-dense/long-run implementation to opus xhigh), a browser-automation delegation nudge for sustained agent-browser/playwright work driven inline on fable, code-review + docs-freshness gates, a detour check-in nudge, blocking verbose-comment enforcement (comment runs over 3 lines / 200 chars denied on the post-edit text; doc comments are grammar-classified — native doc markers or declaration adjacency — and keep only their opening paragraph carved out, under a 6-line / 400-char ceiling; a comment-density warn), a first-turn nudge to batch-load the always-used deferred tools in one ToolSearch call, and delegated-prose nudges that catch doc writing handed to a subagent or workflow without the writing-docs skill, and blocking rm safety (repo-root deletes are denied everywhere, and on the macOS rewrite path so are the filesystem root, home directories, and any directory that contains a git/jj repository; on macOS, outside-repo literal deletes and globs whose at-most-10 matches are each verified are rewritten to trash when available, while oversized or unverifiable globs remain denied everywhere; risky deletes hidden inside `sh`/`bash -c` payloads and `eval` are checked by descending into the nested command; temp/scratch paths exempt)."
|
|
5
|
+
hooks = "."
|
|
6
|
+
nlp = true
|