capt-hook 3.3.2__tar.gz → 3.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.
Files changed (93) hide show
  1. capt_hook-3.4.0/PKG-INFO +136 -0
  2. capt_hook-3.4.0/README.md +89 -0
  3. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/cli.py +12 -0
  4. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/review/cli.py +11 -0
  5. capt_hook-3.4.0/captain_hook/review/dashboard.py +208 -0
  6. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/review/store.py +112 -16
  7. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/review/sync.py +5 -2
  8. {capt_hook-3.3.2 → capt_hook-3.4.0}/pyproject.toml +2 -1
  9. capt_hook-3.3.2/PKG-INFO +0 -152
  10. capt_hook-3.3.2/README.md +0 -106
  11. {capt_hook-3.3.2 → capt_hook-3.4.0}/LICENSE +0 -0
  12. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/.claude-plugin/plugin.json +0 -0
  13. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/__init__.py +0 -0
  14. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/__main__.py +0 -0
  15. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/app.py +0 -0
  16. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/classifiers/__init__.py +0 -0
  17. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/classifiers/conductor.py +0 -0
  18. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/classifiers/droid.py +0 -0
  19. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/classifiers/native.py +0 -0
  20. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/command.py +0 -0
  21. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/conditions.py +0 -0
  22. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/context.py +0 -0
  23. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/decisions.py +0 -0
  24. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/dispatch.py +0 -0
  25. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/events.py +0 -0
  26. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/file.py +0 -0
  27. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/llm/__init__.py +0 -0
  28. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/loader.py +0 -0
  29. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/log.py +0 -0
  30. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/packs/__init__.py +0 -0
  31. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/packs/general/capt-hook.toml +0 -0
  32. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/packs/general/commands.py +0 -0
  33. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/packs/general/docs.py +0 -0
  34. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/packs/general/plans.py +0 -0
  35. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/packs/general/prompts.py +0 -0
  36. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/packs/general/review.py +0 -0
  37. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/packs/general/stewardship.py +0 -0
  38. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/packs/general/tasks.py +0 -0
  39. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/packs/manager.py +0 -0
  40. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/packs/python/capt-hook.toml +0 -0
  41. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/packs/python/style.py +0 -0
  42. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/packs/python/testing.py +0 -0
  43. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/packs/python/toolchain.py +0 -0
  44. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/primitives/__init__.py +0 -0
  45. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/primitives/commands.py +0 -0
  46. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/primitives/lint.py +0 -0
  47. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/primitives/llm.py +0 -0
  48. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/primitives/nudge.py +0 -0
  49. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/primitives/workflow.py +0 -0
  50. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/prompt.py +0 -0
  51. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/py.typed +0 -0
  52. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/review/__init__.py +0 -0
  53. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/review/fix.py +0 -0
  54. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/review/formats.py +0 -0
  55. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/review/judge.py +0 -0
  56. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/review/pipeline.py +0 -0
  57. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/review/repo.py +0 -0
  58. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/review/scan.py +0 -0
  59. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/review/settings.py +0 -0
  60. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/session.py +0 -0
  61. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/settings.py +0 -0
  62. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/signals/__init__.py +0 -0
  63. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/signals/nlp.py +0 -0
  64. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
  65. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
  66. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
  67. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
  68. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
  69. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
  70. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
  71. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
  72. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
  73. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
  74. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
  75. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
  76. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
  77. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/state.py +0 -0
  78. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/style/__init__.py +0 -0
  79. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/style/matchers.py +0 -0
  80. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/style/scope.py +0 -0
  81. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/style/types.py +0 -0
  82. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/tasks.py +0 -0
  83. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
  84. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/testing/__init__.py +0 -0
  85. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/testing/helpers.py +0 -0
  86. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/testing/session_cache.py +0 -0
  87. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/testing/types.py +0 -0
  88. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/tests/__init__.py +0 -0
  89. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/tests/helpers.py +0 -0
  90. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/types.py +0 -0
  91. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/util/__init__.py +0 -0
  92. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/util/model_cache.py +0 -0
  93. {capt_hook-3.3.2 → capt_hook-3.4.0}/captain_hook/utils.py +0 -0
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: capt-hook
3
+ Version: 3.4.0
4
+ Summary: Declarative hook framework for Claude Code
5
+ Keywords: claude,claude-code,hooks,llm,agents,guardrails,cli
6
+ Author: Yasyf Mohamedali
7
+ Author-email: Yasyf Mohamedali <yasyfm@gmail.com>
8
+ License-Expression: PolyForm-Noncommercial-1.0.0
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Topic :: Software Development :: Quality Assurance
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Classifier: Typing :: Typed
19
+ Requires-Dist: cc-transcript>=3.2,<4
20
+ Requires-Dist: pydantic>=2.0
21
+ Requires-Dist: pydantic-settings>=2.0
22
+ Requires-Dist: tree-sitter>=0.24
23
+ Requires-Dist: tree-sitter-bash>=0.23
24
+ Requires-Dist: funcy>=2.0
25
+ Requires-Dist: spacy>=3.7
26
+ Requires-Dist: click>=8
27
+ Requires-Dist: rich>=13
28
+ Requires-Dist: orjsonl>=1.0
29
+ Requires-Dist: wn>=1.1.0
30
+ Requires-Dist: lazy-object-proxy>=1.12.0
31
+ Requires-Dist: filelock>=3
32
+ Requires-Dist: loguru>=0.7.3
33
+ Requires-Dist: spawnllm>=0.1.3
34
+ Requires-Dist: pytest>=8.0 ; extra == 'dev'
35
+ Requires-Dist: pytest-asyncio>=0.24 ; extra == 'dev'
36
+ Requires-Dist: pyright>=1.1 ; extra == 'dev'
37
+ Requires-Dist: pyyaml>=6 ; extra == 'dev'
38
+ Requires-Dist: ruff>=0.8 ; extra == 'dev'
39
+ Requires-Python: >=3.13
40
+ Project-URL: Homepage, https://github.com/yasyf/captain-hook
41
+ Project-URL: Documentation, https://yasyf.github.io/captain-hook/
42
+ Project-URL: Repository, https://github.com/yasyf/captain-hook
43
+ Project-URL: Issues, https://github.com/yasyf/captain-hook/issues
44
+ Project-URL: Changelog, https://github.com/yasyf/captain-hook/blob/main/CHANGELOG.md
45
+ Provides-Extra: dev
46
+ Description-Content-Type: text/markdown
47
+
48
+ # captain-hook
49
+
50
+ ![captain-hook banner](https://github.com/yasyf/captain-hook/raw/main/docs/assets/readme-banner.webp)
51
+
52
+ [![PyPI](https://img.shields.io/pypi/v/capt-hook.svg)](https://pypi.org/project/capt-hook/)
53
+ [![Python](https://img.shields.io/pypi/pyversions/capt-hook.svg)](https://pypi.org/project/capt-hook/)
54
+ [![Docs](https://github.com/yasyf/captain-hook/actions/workflows/docs.yml/badge.svg)](https://yasyf.github.io/captain-hook/)
55
+ [![License: PolyForm Noncommercial](https://img.shields.io/badge/License-PolyForm_Noncommercial_1.0.0-blue.svg)](https://github.com/yasyf/captain-hook/blob/main/LICENSE)
56
+
57
+ Guardrails for Claude Code, written as typed, testable data — and learned from the corrections you give Claude.
58
+
59
+ A captain-hook hook is declarative Python: an event, some conditions, an action. Block a footgun before it runs, nudge the agent off a bad pattern, gate "done" until the tests pass. Then captain-hook closes the loop: it reads the corrections you give Claude as you work and opens pull requests that codify the durable ones as new hooks. You write the first few; it writes the rest.
60
+
61
+ ## Install
62
+
63
+ captain-hook needs no install — it runs through [uvx](https://docs.astral.sh/uv/). From your project root:
64
+
65
+ ```bash
66
+ uvx capt-hook init
67
+ ```
68
+
69
+ `init` scaffolds `.claude/hooks/`, wires Claude Code's settings, installs the bundled skills, and arms the [session reviewer](#it-learns-from-your-corrections). Or install the plugin and let Claude do it. Run `/plugin marketplace add yasyf/captain-hook`, then ask Claude to "set up captain hook".
70
+
71
+ ## Your first hook
72
+
73
+ A hook is an event, some conditions, and an action. This one stops the agent from finishing a UI change it never looked at:
74
+
75
+ ```python
76
+ # .claude/hooks/visual_review.py
77
+ from captain_hook import gate, TouchedFile, UsedSkill
78
+
79
+ gate(
80
+ "You edited UI files. Open them with agent-browser and verify they render before finishing.",
81
+ only_if=[TouchedFile("**/src/routes/**", "**/src/components/**")],
82
+ skip_if=[UsedSkill("agent-browser")],
83
+ )
84
+ ```
85
+
86
+ `only_if` arms the gate only when UI files changed; `skip_if` stands it down once the agent has done the review. Conditions match tools, files, commands, and even which skills the agent used.
87
+
88
+ ## It learns from your corrections
89
+
90
+ Most hooks you'll never write by hand.
91
+
92
+ The corrections you give Claude as you work are exactly the rules a hook should enforce: "never force-push", "use `uv`, not `pip`", "you weakened that test". Writing the hook by hand is friction you skip in the moment, so the **session reviewer** notices for you. When a session ends, it reads the transcript, finds the durable corrections and the hooks that misfired, judges which ones are standing rules and which are one-offs, and once a pattern proves itself across sessions, opens a pull request that adds the hook — or fixes the one that misfired. You review the PR like any other.
93
+
94
+ It's on by default after `init`. Turn it off for a repo with `uvx capt-hook review disable`. The [session reviewer guide](https://yasyf.github.io/captain-hook/docs/guide/session-reviewer.html) covers the prerequisites (an authenticated `claude` and `gh`) and the `HOOKS_REVIEW_*` thresholds.
95
+
96
+ ## Tested like code
97
+
98
+ Every deterministic hook carries inline tests, so a broken hook fails like broken code:
99
+
100
+ ```python
101
+ # .claude/hooks/safety.py
102
+ from captain_hook import Allow, Block, Input, block_command
103
+
104
+ block_command(
105
+ ["git", "stash"],
106
+ reason="Use the team's VCS workflow for shelving changes",
107
+ hint="Commit a WIP change instead of stashing",
108
+ tests={
109
+ Input(command="git stash"): Block(),
110
+ Input(command="git status"): Allow(),
111
+ },
112
+ )
113
+ ```
114
+
115
+ Run them from your project root, where `--hooks` defaults to `.claude/hooks`:
116
+
117
+ ```bash
118
+ uvx capt-hook test
119
+ ```
120
+
121
+ Wire that into CI and you catch a broken hook the way you catch broken code.
122
+
123
+ ## What it's for
124
+
125
+ - Block footguns before they run on `PreToolUse`: force-push, `rm -rf`, package-manager traps.
126
+ - Steer the agent with feedback that fires on the patterns it actually emits: repeated failures, weakened tests, missed conventions.
127
+ - Hold the line on multi-step work with Stop gates and artifact checks, so the agent can't call it "done" before the tests run or the report's written.
128
+ - Keep all of it testable; every hook ships with inline tests that run in CI.
129
+
130
+ ## Docs
131
+
132
+ [Read the docs](https://yasyf.github.io/captain-hook/) for the full guide to conditions, primitives, LLM hooks, workflows, state, and real-world patterns. To work on captain-hook itself, see the [development guide](https://yasyf.github.io/captain-hook/docs/development/).
133
+
134
+ ## License
135
+
136
+ Licensed under [PolyForm Noncommercial 1.0.0](LICENSE), free for noncommercial use.
@@ -0,0 +1,89 @@
1
+ # captain-hook
2
+
3
+ ![captain-hook banner](https://github.com/yasyf/captain-hook/raw/main/docs/assets/readme-banner.webp)
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/capt-hook.svg)](https://pypi.org/project/capt-hook/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/capt-hook.svg)](https://pypi.org/project/capt-hook/)
7
+ [![Docs](https://github.com/yasyf/captain-hook/actions/workflows/docs.yml/badge.svg)](https://yasyf.github.io/captain-hook/)
8
+ [![License: PolyForm Noncommercial](https://img.shields.io/badge/License-PolyForm_Noncommercial_1.0.0-blue.svg)](https://github.com/yasyf/captain-hook/blob/main/LICENSE)
9
+
10
+ Guardrails for Claude Code, written as typed, testable data — and learned from the corrections you give Claude.
11
+
12
+ A captain-hook hook is declarative Python: an event, some conditions, an action. Block a footgun before it runs, nudge the agent off a bad pattern, gate "done" until the tests pass. Then captain-hook closes the loop: it reads the corrections you give Claude as you work and opens pull requests that codify the durable ones as new hooks. You write the first few; it writes the rest.
13
+
14
+ ## Install
15
+
16
+ captain-hook needs no install — it runs through [uvx](https://docs.astral.sh/uv/). From your project root:
17
+
18
+ ```bash
19
+ uvx capt-hook init
20
+ ```
21
+
22
+ `init` scaffolds `.claude/hooks/`, wires Claude Code's settings, installs the bundled skills, and arms the [session reviewer](#it-learns-from-your-corrections). Or install the plugin and let Claude do it. Run `/plugin marketplace add yasyf/captain-hook`, then ask Claude to "set up captain hook".
23
+
24
+ ## Your first hook
25
+
26
+ A hook is an event, some conditions, and an action. This one stops the agent from finishing a UI change it never looked at:
27
+
28
+ ```python
29
+ # .claude/hooks/visual_review.py
30
+ from captain_hook import gate, TouchedFile, UsedSkill
31
+
32
+ gate(
33
+ "You edited UI files. Open them with agent-browser and verify they render before finishing.",
34
+ only_if=[TouchedFile("**/src/routes/**", "**/src/components/**")],
35
+ skip_if=[UsedSkill("agent-browser")],
36
+ )
37
+ ```
38
+
39
+ `only_if` arms the gate only when UI files changed; `skip_if` stands it down once the agent has done the review. Conditions match tools, files, commands, and even which skills the agent used.
40
+
41
+ ## It learns from your corrections
42
+
43
+ Most hooks you'll never write by hand.
44
+
45
+ The corrections you give Claude as you work are exactly the rules a hook should enforce: "never force-push", "use `uv`, not `pip`", "you weakened that test". Writing the hook by hand is friction you skip in the moment, so the **session reviewer** notices for you. When a session ends, it reads the transcript, finds the durable corrections and the hooks that misfired, judges which ones are standing rules and which are one-offs, and once a pattern proves itself across sessions, opens a pull request that adds the hook — or fixes the one that misfired. You review the PR like any other.
46
+
47
+ It's on by default after `init`. Turn it off for a repo with `uvx capt-hook review disable`. The [session reviewer guide](https://yasyf.github.io/captain-hook/docs/guide/session-reviewer.html) covers the prerequisites (an authenticated `claude` and `gh`) and the `HOOKS_REVIEW_*` thresholds.
48
+
49
+ ## Tested like code
50
+
51
+ Every deterministic hook carries inline tests, so a broken hook fails like broken code:
52
+
53
+ ```python
54
+ # .claude/hooks/safety.py
55
+ from captain_hook import Allow, Block, Input, block_command
56
+
57
+ block_command(
58
+ ["git", "stash"],
59
+ reason="Use the team's VCS workflow for shelving changes",
60
+ hint="Commit a WIP change instead of stashing",
61
+ tests={
62
+ Input(command="git stash"): Block(),
63
+ Input(command="git status"): Allow(),
64
+ },
65
+ )
66
+ ```
67
+
68
+ Run them from your project root, where `--hooks` defaults to `.claude/hooks`:
69
+
70
+ ```bash
71
+ uvx capt-hook test
72
+ ```
73
+
74
+ Wire that into CI and you catch a broken hook the way you catch broken code.
75
+
76
+ ## What it's for
77
+
78
+ - Block footguns before they run on `PreToolUse`: force-push, `rm -rf`, package-manager traps.
79
+ - Steer the agent with feedback that fires on the patterns it actually emits: repeated failures, weakened tests, missed conventions.
80
+ - Hold the line on multi-step work with Stop gates and artifact checks, so the agent can't call it "done" before the tests run or the report's written.
81
+ - Keep all of it testable; every hook ships with inline tests that run in CI.
82
+
83
+ ## Docs
84
+
85
+ [Read the docs](https://yasyf.github.io/captain-hook/) for the full guide to conditions, primitives, LLM hooks, workflows, state, and real-world patterns. To work on captain-hook itself, see the [development guide](https://yasyf.github.io/captain-hook/docs/development/).
86
+
87
+ ## License
88
+
89
+ Licensed under [PolyForm Noncommercial 1.0.0](LICENSE), free for noncommercial use.
@@ -527,6 +527,18 @@ def logs(session: str | None, tail: int | None) -> None:
527
527
  show_logs(session=session, tail=tail)
528
528
 
529
529
 
530
+ @cli.command()
531
+ @click.option("--repo", "repo_", default=None, help="Repo key (default: the current repo)")
532
+ @click.option("--sync/--no-sync", default=True, help="Refresh open PR states from GitHub in the background")
533
+ @click.pass_obj
534
+ def status(state: CliState, repo_: str | None, sync: bool) -> None:
535
+ """Show the corrections the session reviewer is tracking and the hook PRs they would open."""
536
+ from captain_hook.review.cli import resolve_repo
537
+ from captain_hook.review.dashboard import status_command
538
+
539
+ status_command(resolve_repo(repo_, state.root), sync=sync)
540
+
541
+
530
542
  @cli.group()
531
543
  def skills() -> None:
532
544
  """Manage the bundled Claude Code skills."""
@@ -198,6 +198,17 @@ def triage(limit: int | None) -> None:
198
198
  click.echo(f"judged {report.judged}, failed {report.failed}, pending {report.pending}")
199
199
 
200
200
 
201
+ @review.command(name="status")
202
+ @click.option("--repo", "repo_", default=None, help="Repo key (default: the current repo)")
203
+ @click.option("--sync/--no-sync", default=True, help="Refresh open PR states from GitHub in the background")
204
+ @click.pass_obj
205
+ def status(state: CliState, repo_: str | None, sync: bool) -> None:
206
+ """Show the tracked corrections, their progress toward a PR, and open PR status."""
207
+ from captain_hook.review.dashboard import status_command
208
+
209
+ status_command(resolve_repo(repo_, state.root), sync=sync)
210
+
211
+
201
212
  @review.command(name="list")
202
213
  @click.option("--repo", "repo_", default=None, help="Repo key (default: the current repo)")
203
214
  @click.pass_obj
@@ -0,0 +1,208 @@
1
+ """The ``capt-hook status`` dashboard: the corrections lifecycle, rendered.
2
+
3
+ Reads the reviewer's :class:`~captain_hook.review.store.ReviewStore` and renders
4
+ every candidate the reviewer tracks, bucketed by lifecycle stage — watching
5
+ (building toward the bar), eligible (a PR opens next session), PR open, and the
6
+ merged/closed/stale outcomes. Each row shows kind-aware progress toward its PR
7
+ thresholds and the one-sentence summary of what its PR would do. Open PRs are
8
+ shown from the last-synced state first, then refreshed against GitHub in the
9
+ background so the view appears instantly and updates when ``gh`` returns.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ from datetime import UTC, datetime
16
+ from enum import StrEnum
17
+ from typing import TYPE_CHECKING
18
+
19
+ from rich.console import Console, Group
20
+ from rich.spinner import Spinner
21
+ from rich.text import Text
22
+
23
+ from captain_hook.review.store import CandidateKind, CandidateStatus
24
+
25
+ if TYPE_CHECKING:
26
+ from rich.console import RenderableType
27
+
28
+ from captain_hook.review.repo import RepoKey
29
+ from captain_hook.review.settings import ReviewSettings
30
+ from captain_hook.review.store import CandidateView
31
+
32
+ BAR_FILLED = "█"
33
+ BAR_EMPTY = "░"
34
+ DETAIL_WIDTH = 80
35
+ KIND_STYLE = {CandidateKind.CREATE: "cyan", CandidateKind.FIX: "magenta"}
36
+
37
+
38
+ class Stage(StrEnum):
39
+ """A candidate's dashboard bucket — its lifecycle status, with watching split by eligibility."""
40
+
41
+ WATCHING = "watching"
42
+ ELIGIBLE = "eligible"
43
+ PR_OPEN = "pr_open"
44
+ ACCEPTED = "accepted"
45
+ REJECTED = "rejected"
46
+ STALE = "stale"
47
+
48
+
49
+ SECTIONS: tuple[tuple[Stage, str, str, str], ...] = (
50
+ (Stage.WATCHING, "WATCHING", "building toward the bar", "yellow"),
51
+ (Stage.ELIGIBLE, "ELIGIBLE", "a PR opens next session", "green"),
52
+ (Stage.PR_OPEN, "PR OPEN", "pull request awaiting your review", "blue"),
53
+ (Stage.ACCEPTED, "ACCEPTED", "PR merged", "green"),
54
+ (Stage.REJECTED, "REJECTED", "PR closed", "red"),
55
+ (Stage.STALE, "STALE", "PR open too long", "bright_black"),
56
+ )
57
+
58
+
59
+ def stage_of(view: CandidateView) -> Stage:
60
+ """Buckets a candidate: its status, with ``watching`` split into eligible vs not."""
61
+ match CandidateStatus(str(view.row["status"])):
62
+ case CandidateStatus.PR_OPEN:
63
+ return Stage.PR_OPEN
64
+ case CandidateStatus.ACCEPTED:
65
+ return Stage.ACCEPTED
66
+ case CandidateStatus.REJECTED:
67
+ return Stage.REJECTED
68
+ case CandidateStatus.STALE:
69
+ return Stage.STALE
70
+ case CandidateStatus.WATCHING:
71
+ return Stage.ELIGIBLE if view.eligible else Stage.WATCHING
72
+
73
+
74
+ def trim(text: str, *, width: int = DETAIL_WIDTH) -> str:
75
+ return flat if len(flat := " ".join(text.split())) <= width else flat[: width - 1] + "…"
76
+
77
+
78
+ def bar(done: int, need: int, *, width: int = 5) -> str:
79
+ cells = min(need, width)
80
+ filled = min(cells, round(done / need * cells)) if need else cells
81
+ return BAR_FILLED * filled + BAR_EMPTY * (cells - filled)
82
+
83
+
84
+ def targets(view: CandidateView, settings: ReviewSettings) -> tuple[tuple[str, int, int], ...]:
85
+ t = view.threshold
86
+ match t.kind:
87
+ case CandidateKind.CREATE:
88
+ return (("sessions", t.sessions, settings.min_sessions), ("days", t.days, settings.min_days))
89
+ case CandidateKind.FIX:
90
+ return (("sessions", t.sessions, settings.min_sessions_fix), ("days", t.days, settings.min_days_fix))
91
+
92
+
93
+ def progress_text(view: CandidateView, settings: ReviewSettings) -> str:
94
+ return " ".join(
95
+ f"{label} {bar(done, need)} {done}/{need}" for label, done, need in targets(view, settings) if need > 0
96
+ )
97
+
98
+
99
+ def pr_description(view: CandidateView) -> str:
100
+ """The one-line summary of what this candidate's PR would (or did) do."""
101
+ row = view.row
102
+ detail = trim(view.summary or str(row["sample_text"] or ""))
103
+ match CandidateKind(str(row["candidate_kind"])):
104
+ case CandidateKind.CREATE:
105
+ return f'would add a hook: "{detail}"' if detail else "would add a hook for this correction"
106
+ case CandidateKind.FIX:
107
+ tail = view.summary or (
108
+ f"regression test for {row['misfire_class']}" if row["misfire_class"] else "regression test for the misfire"
109
+ )
110
+ return f"would fix {row['target_hook_name']} ({row['target_source_file']}): {trim(str(tail))}"
111
+
112
+
113
+ def age_days(row: dict[str, object]) -> int | None:
114
+ if not (opened := row["pr_opened_at"]):
115
+ return None
116
+ return (datetime.now(UTC) - datetime.fromisoformat(str(opened))).days
117
+
118
+
119
+ def pr_link(view: CandidateView) -> str:
120
+ url = str(view.row["pr_url"] or "(no url)")
121
+ return f"{url} · {days}d open" if stage_of(view) is Stage.PR_OPEN and (days := age_days(view.row)) is not None else url
122
+
123
+
124
+ def lead_detail(view: CandidateView, settings: ReviewSettings) -> str:
125
+ match stage_of(view):
126
+ case Stage.WATCHING:
127
+ return progress_text(view, settings)
128
+ case Stage.ELIGIBLE:
129
+ return f"ready · {progress_text(view, settings)}"
130
+ case _:
131
+ return pr_link(view)
132
+
133
+
134
+ def candidate_block(view: CandidateView, settings: ReviewSettings) -> RenderableType:
135
+ kind = CandidateKind(str(view.row["candidate_kind"]))
136
+ return Group(
137
+ Text.assemble(
138
+ (f" #{view.row['id']}", "bold"),
139
+ " ",
140
+ (kind.value.ljust(6), KIND_STYLE[kind]),
141
+ " ",
142
+ lead_detail(view, settings),
143
+ ),
144
+ Text(f" {pr_description(view)}", style="dim"),
145
+ )
146
+
147
+
148
+ def header(repo: RepoKey, views: list[CandidateView], settings: ReviewSettings, *, watching: bool) -> RenderableType:
149
+ open_n = sum(1 for v in views if stage_of(v) is Stage.PR_OPEN)
150
+ line = Text.assemble(
151
+ ("captain-hook", "bold"),
152
+ (" · ", "dim"),
153
+ (str(repo), "cyan"),
154
+ " ",
155
+ (f"[{'watching' if watching else 'not watching'}]", "green" if watching else "yellow"),
156
+ " ",
157
+ (f"PR slots {open_n}/{settings.max_open_prs}", "dim"),
158
+ )
159
+ if watching:
160
+ return line
161
+ return Group(line, Text(" run `capt-hook review enable` to start tracking this repo.", style="dim"))
162
+
163
+
164
+ def render(
165
+ views: list[CandidateView], *, repo: RepoKey, settings: ReviewSettings, watching: bool, syncing: bool = False
166
+ ) -> RenderableType:
167
+ """The whole dashboard frame: header, then a section per non-empty lifecycle stage."""
168
+ sections = [
169
+ block
170
+ for stage, title, desc, style in SECTIONS
171
+ if (members := [v for v in views if stage_of(v) is stage])
172
+ for block in (
173
+ Text.assemble((title, f"bold {style}"), (f" {desc}", "dim")),
174
+ *(candidate_block(v, settings) for v in members),
175
+ Text(""),
176
+ )
177
+ ]
178
+ empty = [] if views else [Text("No corrections tracked yet — they appear here as you correct Claude.", style="dim")]
179
+ spinner = [Spinner("dots", text=Text("syncing open PRs with GitHub…", style="dim"))] if syncing else []
180
+ return Group(header(repo, views, settings, watching=watching), Text(""), *sections, *empty, *spinner)
181
+
182
+
183
+ async def run_status(repo: RepoKey, *, sync: bool) -> None:
184
+ """Renders the dashboard for ``repo``, refreshing open-PR state in the background when ``sync``."""
185
+ from rich.live import Live
186
+
187
+ from captain_hook.review.judge import REVIEW_PROMPT_VERSION
188
+ from captain_hook.review.settings import ReviewSettings
189
+ from captain_hook.review.store import ReviewStore
190
+ from captain_hook.review.sync import sync_open_prs
191
+
192
+ settings = ReviewSettings()
193
+ console = Console()
194
+ async with await ReviewStore.open(settings.db_path) as store:
195
+ watching = await store.watching(repo)
196
+ views = await store.overview(repo, settings=settings, prompt_version=REVIEW_PROMPT_VERSION)
197
+ if not (sync and any(stage_of(v) is Stage.PR_OPEN for v in views)):
198
+ console.print(render(views, repo=repo, settings=settings, watching=watching))
199
+ return
200
+ with Live(render(views, repo=repo, settings=settings, watching=watching, syncing=True), console=console) as live:
201
+ await sync_open_prs(store, repo, settings=settings)
202
+ fresh = await store.overview(repo, settings=settings, prompt_version=REVIEW_PROMPT_VERSION)
203
+ live.update(render(fresh, repo=repo, settings=settings, watching=watching))
204
+
205
+
206
+ def status_command(repo: RepoKey, *, sync: bool) -> None:
207
+ """The synchronous CLI boundary for ``review status`` / ``capt-hook status``."""
208
+ asyncio.run(run_status(repo, sync=sync))
@@ -113,6 +113,22 @@ SELECT c.*,
113
113
  FROM candidates c
114
114
  """
115
115
 
116
+ PR_SUMMARY_QUERY = """
117
+ WITH latest AS (
118
+ SELECT v.dedup_key, v.{accepted} AS accepted, v.{summary} AS summary, v.confidence, ROW_NUMBER() OVER (
119
+ PARTITION BY v.dedup_key ORDER BY v.judged_at DESC, v.id DESC
120
+ ) AS rn
121
+ FROM {table} v
122
+ WHERE v.role = 'judge' AND v.prompt_version = ?
123
+ )
124
+ SELECT l.summary AS summary
125
+ FROM candidate_observations o
126
+ JOIN latest l ON l.dedup_key = o.dedup_key AND l.rn = 1
127
+ WHERE o.candidate_id = ? AND l.accepted = 1 AND l.confidence >= ?
128
+ ORDER BY l.confidence DESC, o.id DESC
129
+ LIMIT 1
130
+ """
131
+
116
132
 
117
133
  class InvalidTransition(Exception):
118
134
  """Raised when a candidate status move is outside :data:`TRANSITIONS`."""
@@ -175,6 +191,47 @@ class ThresholdStatus:
175
191
  single_observation: bool
176
192
 
177
193
 
194
+ def crosses_thresholds(status: ThresholdStatus, *, settings: ReviewSettings) -> bool:
195
+ """Whether a candidate's judge-accepted evidence clears its kind's PR thresholds.
196
+
197
+ The single eligibility predicate, shared by :meth:`ReviewStore.eligible` and
198
+ the status dashboard so a candidate shown as eligible is exactly one the
199
+ reviewer would act on. Create candidates need ``min_sessions`` distinct
200
+ judge-accepted sessions across ``min_days`` distinct UTC days; fix candidates
201
+ need the ``min_sessions_fix``/``min_days_fix`` pair or one observation that is
202
+ both judge-accepted and heuristically at least ``min_confidence_fix_single``.
203
+ Both require the repo watched and a free slot under ``max_open_prs``.
204
+ """
205
+ if status.status != CandidateStatus.WATCHING or not status.watching or status.open_prs >= settings.max_open_prs:
206
+ return False
207
+ match status.kind:
208
+ case CandidateKind.CREATE:
209
+ return status.sessions >= settings.min_sessions and status.days >= settings.min_days
210
+ case CandidateKind.FIX:
211
+ return (
212
+ status.sessions >= settings.min_sessions_fix and status.days >= settings.min_days_fix
213
+ ) or status.single_observation
214
+
215
+
216
+ @dataclass(frozen=True, slots=True)
217
+ class CandidateView:
218
+ """One candidate's full dashboard record: its row, evidence counts, eligibility, and the PR it would open.
219
+
220
+ Attributes:
221
+ row: The :meth:`ReviewStore.candidates` row (status, kind, ``pr_url``,
222
+ ``sample_text``, ``observations``, and the fix targets).
223
+ threshold: The judge-accepted evidence counts behind the eligibility call.
224
+ eligible: Whether :func:`crosses_thresholds` accepts ``threshold``.
225
+ summary: The highest-confidence accepted verdict's one-sentence summary —
226
+ what the candidate's PR would do — or ``None`` while still unjudged.
227
+ """
228
+
229
+ row: dict[str, object]
230
+ threshold: ThresholdStatus
231
+ eligible: bool
232
+ summary: str | None
233
+
234
+
178
235
  class ReviewStore(VerdictStoreMixin, FeedbackStore):
179
236
  """The session reviewer's persistent store over a :class:`FileStateStore`.
180
237
 
@@ -411,25 +468,64 @@ class ReviewStore(VerdictStoreMixin, FeedbackStore):
411
468
  async def eligible(self, candidate_id: int, *, settings: ReviewSettings, prompt_version: int) -> bool:
412
469
  """Returns whether a candidate's judge-accepted evidence crosses its thresholds.
413
470
 
414
- Create candidates need ``min_sessions`` distinct judge-accepted sessions
415
- across ``min_days`` distinct UTC days. Fix candidates need the
416
- ``min_sessions_fix``/``min_days_fix`` pair, or one observation that is
417
- both judge-accepted and heuristically at least
418
- ``min_confidence_fix_single``. Both kinds require the repo watched and a
419
- free slot under ``max_open_prs``.
471
+ Delegates to :func:`crosses_thresholds` over the candidate's
472
+ :meth:`threshold_status`, so the dashboard and the reviewer agree on what
473
+ is eligible.
420
474
 
421
475
  Args:
422
476
  candidate_id: The candidate to check.
423
477
  settings: The thresholds and judge knobs to check under.
424
478
  prompt_version: The judge prompt version whose verdicts apply.
425
479
  """
426
- status = await self.threshold_status(candidate_id, settings=settings, prompt_version=prompt_version)
427
- if status.status != CandidateStatus.WATCHING or not status.watching or status.open_prs >= settings.max_open_prs:
428
- return False
429
- match status.kind:
430
- case CandidateKind.CREATE:
431
- return status.sessions >= settings.min_sessions and status.days >= settings.min_days
432
- case CandidateKind.FIX:
433
- return (
434
- status.sessions >= settings.min_sessions_fix and status.days >= settings.min_days_fix
435
- ) or status.single_observation
480
+ return crosses_thresholds(
481
+ await self.threshold_status(candidate_id, settings=settings, prompt_version=prompt_version),
482
+ settings=settings,
483
+ )
484
+
485
+ async def pr_summary(self, candidate_id: int, *, settings: ReviewSettings, prompt_version: int) -> str | None:
486
+ """Returns the candidate's most-confident accepted verdict summary — what its PR would do.
487
+
488
+ Reads the same judge-accepted observations the thresholds count and
489
+ returns the highest-confidence verdict's one-sentence summary, or ``None``
490
+ while no observation is judged-accepted yet.
491
+
492
+ Args:
493
+ candidate_id: The candidate to describe.
494
+ settings: The judge knobs supplying ``min_judge_confidence``.
495
+ prompt_version: The judge prompt version whose verdicts apply.
496
+ """
497
+ cur = await self.store.conn.execute(
498
+ PR_SUMMARY_QUERY.format(
499
+ table=self.VERDICT_TABLE, accepted=self.ACCEPTED_COLUMN, summary=self.SUMMARY_COLUMN
500
+ ),
501
+ (prompt_version, candidate_id, settings.min_judge_confidence),
502
+ )
503
+ return str(rows[0]["summary"]) if (rows := [dict(row) async for row in cur]) else None
504
+
505
+ async def candidate_view(
506
+ self, row: dict[str, object], *, settings: ReviewSettings, prompt_version: int
507
+ ) -> CandidateView:
508
+ """Assembles one :class:`CandidateView` from a :meth:`candidates` row."""
509
+ candidate_id = int(str(row["id"]))
510
+ threshold = await self.threshold_status(candidate_id, settings=settings, prompt_version=prompt_version)
511
+ return CandidateView(
512
+ row=row,
513
+ threshold=threshold,
514
+ eligible=crosses_thresholds(threshold, settings=settings),
515
+ summary=await self.pr_summary(candidate_id, settings=settings, prompt_version=prompt_version),
516
+ )
517
+
518
+ async def overview(
519
+ self, repo: RepoKey | None = None, *, settings: ReviewSettings, prompt_version: int
520
+ ) -> list[CandidateView]:
521
+ """Returns a :class:`CandidateView` per candidate — the status dashboard's whole read.
522
+
523
+ Args:
524
+ repo: When set, restrict to this repo.
525
+ settings: The thresholds and judge knobs to evaluate under.
526
+ prompt_version: The judge prompt version whose verdicts apply.
527
+ """
528
+ return [
529
+ await self.candidate_view(row, settings=settings, prompt_version=prompt_version)
530
+ for row in await self.candidates(repo)
531
+ ]
@@ -8,6 +8,7 @@ skipped so the detached child never dies on it.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import asyncio
11
12
  import json
12
13
  import subprocess
13
14
  from collections import Counter
@@ -75,9 +76,11 @@ async def sync_open_prs(store: ReviewStore, repo: RepoKey, *, settings: ReviewSe
75
76
  The pass's transition counts.
76
77
  """
77
78
  counts: Counter[str] = Counter()
78
- for row in await store.candidates(repo, status=CandidateStatus.PR_OPEN):
79
+ rows = await store.candidates(repo, status=CandidateStatus.PR_OPEN)
80
+ states = await asyncio.gather(*(asyncio.to_thread(gh_pr_state, str(row["pr_url"])) for row in rows))
81
+ for row, state in zip(rows, states, strict=True):
79
82
  candidate_id, url = int(str(row["id"])), str(row["pr_url"])
80
- match gh_pr_state(url):
83
+ match state:
81
84
  case "MERGED":
82
85
  await store.transition(candidate_id, CandidateStatus.ACCEPTED)
83
86
  counts["accepted"] += 1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "capt-hook"
3
- version = "3.3.2"
3
+ version = "3.4.0"
4
4
  description = "Declarative hook framework for Claude Code"
5
5
  readme = "README.md"
6
6
  license = "PolyForm-Noncommercial-1.0.0"
@@ -28,6 +28,7 @@ dependencies = [
28
28
  "funcy>=2.0",
29
29
  "spacy>=3.7",
30
30
  "click>=8",
31
+ "rich>=13",
31
32
  "orjsonl>=1.0",
32
33
  "wn>=1.1.0",
33
34
  "lazy-object-proxy>=1.12.0",
capt_hook-3.3.2/PKG-INFO DELETED
@@ -1,152 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: capt-hook
3
- Version: 3.3.2
4
- Summary: Declarative hook framework for Claude Code
5
- Keywords: claude,claude-code,hooks,llm,agents,guardrails,cli
6
- Author: Yasyf Mohamedali
7
- Author-email: Yasyf Mohamedali <yasyfm@gmail.com>
8
- License-Expression: PolyForm-Noncommercial-1.0.0
9
- License-File: LICENSE
10
- Classifier: Development Status :: 4 - Beta
11
- Classifier: Intended Audience :: Developers
12
- Classifier: Operating System :: OS Independent
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.13
15
- Classifier: Programming Language :: Python :: 3 :: Only
16
- Classifier: Topic :: Software Development :: Quality Assurance
17
- Classifier: Topic :: Software Development :: Testing
18
- Classifier: Typing :: Typed
19
- Requires-Dist: cc-transcript>=3.2,<4
20
- Requires-Dist: pydantic>=2.0
21
- Requires-Dist: pydantic-settings>=2.0
22
- Requires-Dist: tree-sitter>=0.24
23
- Requires-Dist: tree-sitter-bash>=0.23
24
- Requires-Dist: funcy>=2.0
25
- Requires-Dist: spacy>=3.7
26
- Requires-Dist: click>=8
27
- Requires-Dist: orjsonl>=1.0
28
- Requires-Dist: wn>=1.1.0
29
- Requires-Dist: lazy-object-proxy>=1.12.0
30
- Requires-Dist: filelock>=3
31
- Requires-Dist: loguru>=0.7.3
32
- Requires-Dist: spawnllm>=0.1.3
33
- Requires-Dist: pytest>=8.0 ; extra == 'dev'
34
- Requires-Dist: pytest-asyncio>=0.24 ; extra == 'dev'
35
- Requires-Dist: pyright>=1.1 ; extra == 'dev'
36
- Requires-Dist: pyyaml>=6 ; extra == 'dev'
37
- Requires-Dist: ruff>=0.8 ; extra == 'dev'
38
- Requires-Python: >=3.13
39
- Project-URL: Homepage, https://github.com/yasyf/captain-hook
40
- Project-URL: Documentation, https://yasyf.github.io/captain-hook/
41
- Project-URL: Repository, https://github.com/yasyf/captain-hook
42
- Project-URL: Issues, https://github.com/yasyf/captain-hook/issues
43
- Project-URL: Changelog, https://github.com/yasyf/captain-hook/blob/main/CHANGELOG.md
44
- Provides-Extra: dev
45
- Description-Content-Type: text/markdown
46
-
47
- # captain-hook
48
-
49
- ![captain-hook banner](https://github.com/yasyf/captain-hook/raw/main/docs/assets/readme-banner.webp)
50
-
51
- [![PyPI](https://img.shields.io/pypi/v/capt-hook.svg)](https://pypi.org/project/capt-hook/)
52
- [![Python](https://img.shields.io/pypi/pyversions/capt-hook.svg)](https://pypi.org/project/capt-hook/)
53
- [![Docs](https://github.com/yasyf/captain-hook/actions/workflows/docs.yml/badge.svg)](https://yasyf.github.io/captain-hook/)
54
- [![License: PolyForm Noncommercial](https://img.shields.io/badge/License-PolyForm_Noncommercial_1.0.0-blue.svg)](https://github.com/yasyf/captain-hook/blob/main/LICENSE)
55
-
56
- Declarative hook framework for Claude Code. Write hooks as data, test them inline, and ship them to CI in the same shape they run in production.
57
-
58
- ## Quickstart
59
-
60
- No install step — everything runs through [uvx](https://docs.astral.sh/uv/). Pick a front door:
61
-
62
- **From your terminal:**
63
-
64
- ```bash
65
- uvx capt-hook init
66
- ```
67
-
68
- **From inside Claude Code** — install the plugin, then ask Claude to set it up:
69
-
70
- ```
71
- /plugin marketplace add yasyf/captain-hook
72
- /plugin install captain-hook@captain-hook
73
- ```
74
-
75
- > set up captain hook
76
-
77
- Either path lands in the same place: `.claude/hooks/` scaffolded, Claude Code's settings wired, the bundled skills installed, and the [session reviewer](#session-reviewer) watching this repo. `uvx` fetches captain-hook into a throwaway environment, so it never enters your `pyproject.toml` — and every command below works the same way once you prefix it with `uvx`.
78
-
79
- ## Your first hook
80
-
81
- A hook is declarative Python with an event, some conditions, and an action. This one stops the agent from finishing a UI change it never looked at.
82
-
83
- ```python
84
- # .claude/hooks/visual_review.py
85
- from captain_hook import gate, TouchedFile, UsedSkill
86
-
87
- # A Stop gate: before the agent finishes, block if it edited UI files without doing a visual review.
88
- gate(
89
- # the one-line reason shown to the agent when the gate fires
90
- "You edited UI files. Open them with agent-browser and verify they render before finishing.",
91
- # fires only if UI files changed
92
- only_if=[TouchedFile("**/src/routes/**", "**/src/components/**")],
93
- # already reviewed -> don't block
94
- skip_if=[UsedSkill("agent-browser")],
95
- )
96
- ```
97
-
98
- Conditions match tools, files, commands, and even which skills the agent used.
99
-
100
- ## Test your hooks
101
-
102
- Every deterministic hook carries inline tests, so a broken hook fails like broken code. Run them from your project root, where `--hooks` defaults to `.claude/hooks`.
103
-
104
- ```python
105
- # .claude/hooks/safety.py
106
- from captain_hook import Allow, Block, Input, block_command
107
-
108
- block_command(
109
- ["git", "stash"],
110
- reason="Use the team's VCS workflow for shelving changes",
111
- hint="Commit a WIP change instead of stashing",
112
- tests={
113
- Input(command="git stash"): Block(),
114
- Input(command="git status"): Allow(),
115
- },
116
- )
117
- ```
118
-
119
- ```bash
120
- uvx capt-hook test
121
- ```
122
-
123
- `init` already wired Claude Code's settings. Each event runs `uvx capt-hook run <Event>`, with the event JSON arriving on stdin and the verdict written to stdout. Re-run `uvx capt-hook register-hooks` only after you add hooks on a new event; it writes `.claude/settings.local.json` for you.
124
-
125
- ## Session reviewer
126
-
127
- `init` also turns on the **session reviewer**. When a Claude Code session ends, it mines the transcript for the durable corrections you gave and the hooks that misfired, judges each one, and — once a pattern clears its thresholds — opens a pull request that adds a new hook or fixes the one that misfired. You review the PR like any other.
128
-
129
- It's on by default after `init`. Turn it off for a repo with `uvx capt-hook review disable`, or skip it at setup with `uvx capt-hook init --no-review`. The [session reviewer guide](https://yasyf.github.io/captain-hook/docs/guide/session-reviewer.html) covers prerequisites (an authenticated `claude` and `gh`) and the `HOOKS_REVIEW_*` tuning knobs.
130
-
131
- ## Agent Skills
132
-
133
- captain-hook ships two [Agent Skills](https://yasyf.github.io/captain-hook/docs/getting-started/skills.html) so you don't have to write hooks by hand. `bootstrapping-hooks` surveys your repo's docs, CI, and git history and proposes gates and nudges; `translating-styleguides` turns a STYLEGUIDE.md into enforced rules. Both land in `.claude/skills/` via `init` and ship as the plugin in the [Quickstart](#quickstart) — ask Claude to "set up captain hook" and `bootstrapping-hooks` takes it from there.
134
-
135
- ## What this solves
136
-
137
- captain-hook covers these jobs:
138
-
139
- - Block dangerous tool calls before they execute on `PreToolUse`, like force-push, package-manager footguns, and raw `rm -rf`.
140
- - Drive the agent with feedback that fires on the patterns it actually emits, such as repeated failures, weakened tests, and missed conventions.
141
- - Enforce multi-step workflows with Stop gates and artifact validation, so the agent can't declare "done" without running tests, writing a report, or completing a checklist.
142
- - Keep all of the above testable. Every hook ships with inline `tests = {...}` that `uvx capt-hook test` runs in CI, so you catch broken hooks the way you catch broken code.
143
-
144
- ## Docs
145
-
146
- [Read the docs](https://yasyf.github.io/captain-hook/) for the full guide to conditions, primitives, LLM hooks, workflows, state, and real-world patterns.
147
-
148
- For working on captain-hook itself, see the [development guide](https://yasyf.github.io/captain-hook/docs/development/).
149
-
150
- ## License
151
-
152
- Licensed under [PolyForm Noncommercial 1.0.0](LICENSE), free for noncommercial use.
capt_hook-3.3.2/README.md DELETED
@@ -1,106 +0,0 @@
1
- # captain-hook
2
-
3
- ![captain-hook banner](https://github.com/yasyf/captain-hook/raw/main/docs/assets/readme-banner.webp)
4
-
5
- [![PyPI](https://img.shields.io/pypi/v/capt-hook.svg)](https://pypi.org/project/capt-hook/)
6
- [![Python](https://img.shields.io/pypi/pyversions/capt-hook.svg)](https://pypi.org/project/capt-hook/)
7
- [![Docs](https://github.com/yasyf/captain-hook/actions/workflows/docs.yml/badge.svg)](https://yasyf.github.io/captain-hook/)
8
- [![License: PolyForm Noncommercial](https://img.shields.io/badge/License-PolyForm_Noncommercial_1.0.0-blue.svg)](https://github.com/yasyf/captain-hook/blob/main/LICENSE)
9
-
10
- Declarative hook framework for Claude Code. Write hooks as data, test them inline, and ship them to CI in the same shape they run in production.
11
-
12
- ## Quickstart
13
-
14
- No install step — everything runs through [uvx](https://docs.astral.sh/uv/). Pick a front door:
15
-
16
- **From your terminal:**
17
-
18
- ```bash
19
- uvx capt-hook init
20
- ```
21
-
22
- **From inside Claude Code** — install the plugin, then ask Claude to set it up:
23
-
24
- ```
25
- /plugin marketplace add yasyf/captain-hook
26
- /plugin install captain-hook@captain-hook
27
- ```
28
-
29
- > set up captain hook
30
-
31
- Either path lands in the same place: `.claude/hooks/` scaffolded, Claude Code's settings wired, the bundled skills installed, and the [session reviewer](#session-reviewer) watching this repo. `uvx` fetches captain-hook into a throwaway environment, so it never enters your `pyproject.toml` — and every command below works the same way once you prefix it with `uvx`.
32
-
33
- ## Your first hook
34
-
35
- A hook is declarative Python with an event, some conditions, and an action. This one stops the agent from finishing a UI change it never looked at.
36
-
37
- ```python
38
- # .claude/hooks/visual_review.py
39
- from captain_hook import gate, TouchedFile, UsedSkill
40
-
41
- # A Stop gate: before the agent finishes, block if it edited UI files without doing a visual review.
42
- gate(
43
- # the one-line reason shown to the agent when the gate fires
44
- "You edited UI files. Open them with agent-browser and verify they render before finishing.",
45
- # fires only if UI files changed
46
- only_if=[TouchedFile("**/src/routes/**", "**/src/components/**")],
47
- # already reviewed -> don't block
48
- skip_if=[UsedSkill("agent-browser")],
49
- )
50
- ```
51
-
52
- Conditions match tools, files, commands, and even which skills the agent used.
53
-
54
- ## Test your hooks
55
-
56
- Every deterministic hook carries inline tests, so a broken hook fails like broken code. Run them from your project root, where `--hooks` defaults to `.claude/hooks`.
57
-
58
- ```python
59
- # .claude/hooks/safety.py
60
- from captain_hook import Allow, Block, Input, block_command
61
-
62
- block_command(
63
- ["git", "stash"],
64
- reason="Use the team's VCS workflow for shelving changes",
65
- hint="Commit a WIP change instead of stashing",
66
- tests={
67
- Input(command="git stash"): Block(),
68
- Input(command="git status"): Allow(),
69
- },
70
- )
71
- ```
72
-
73
- ```bash
74
- uvx capt-hook test
75
- ```
76
-
77
- `init` already wired Claude Code's settings. Each event runs `uvx capt-hook run <Event>`, with the event JSON arriving on stdin and the verdict written to stdout. Re-run `uvx capt-hook register-hooks` only after you add hooks on a new event; it writes `.claude/settings.local.json` for you.
78
-
79
- ## Session reviewer
80
-
81
- `init` also turns on the **session reviewer**. When a Claude Code session ends, it mines the transcript for the durable corrections you gave and the hooks that misfired, judges each one, and — once a pattern clears its thresholds — opens a pull request that adds a new hook or fixes the one that misfired. You review the PR like any other.
82
-
83
- It's on by default after `init`. Turn it off for a repo with `uvx capt-hook review disable`, or skip it at setup with `uvx capt-hook init --no-review`. The [session reviewer guide](https://yasyf.github.io/captain-hook/docs/guide/session-reviewer.html) covers prerequisites (an authenticated `claude` and `gh`) and the `HOOKS_REVIEW_*` tuning knobs.
84
-
85
- ## Agent Skills
86
-
87
- captain-hook ships two [Agent Skills](https://yasyf.github.io/captain-hook/docs/getting-started/skills.html) so you don't have to write hooks by hand. `bootstrapping-hooks` surveys your repo's docs, CI, and git history and proposes gates and nudges; `translating-styleguides` turns a STYLEGUIDE.md into enforced rules. Both land in `.claude/skills/` via `init` and ship as the plugin in the [Quickstart](#quickstart) — ask Claude to "set up captain hook" and `bootstrapping-hooks` takes it from there.
88
-
89
- ## What this solves
90
-
91
- captain-hook covers these jobs:
92
-
93
- - Block dangerous tool calls before they execute on `PreToolUse`, like force-push, package-manager footguns, and raw `rm -rf`.
94
- - Drive the agent with feedback that fires on the patterns it actually emits, such as repeated failures, weakened tests, and missed conventions.
95
- - Enforce multi-step workflows with Stop gates and artifact validation, so the agent can't declare "done" without running tests, writing a report, or completing a checklist.
96
- - Keep all of the above testable. Every hook ships with inline `tests = {...}` that `uvx capt-hook test` runs in CI, so you catch broken hooks the way you catch broken code.
97
-
98
- ## Docs
99
-
100
- [Read the docs](https://yasyf.github.io/captain-hook/) for the full guide to conditions, primitives, LLM hooks, workflows, state, and real-world patterns.
101
-
102
- For working on captain-hook itself, see the [development guide](https://yasyf.github.io/captain-hook/docs/development/).
103
-
104
- ## License
105
-
106
- Licensed under [PolyForm Noncommercial 1.0.0](LICENSE), free for noncommercial use.
File without changes
File without changes
File without changes