capt-hook 3.7.0__tar.gz → 3.9.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 (94) hide show
  1. {capt_hook-3.7.0 → capt_hook-3.9.0}/PKG-INFO +3 -3
  2. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/packs/general/docs.py +3 -1
  3. capt_hook-3.9.0/captain_hook/packs/go/capt-hook.toml +4 -0
  4. capt_hook-3.9.0/captain_hook/packs/go/testing.py +87 -0
  5. capt_hook-3.9.0/captain_hook/packs/go/toolchain.py +46 -0
  6. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/review/cli.py +35 -3
  7. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/review/scan.py +35 -1
  8. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/review/store.py +29 -0
  9. {capt_hook-3.7.0 → capt_hook-3.9.0}/pyproject.toml +3 -3
  10. {capt_hook-3.7.0 → capt_hook-3.9.0}/LICENSE +0 -0
  11. {capt_hook-3.7.0 → capt_hook-3.9.0}/README.md +0 -0
  12. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/.claude-plugin/plugin.json +0 -0
  13. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/__init__.py +0 -0
  14. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/__main__.py +0 -0
  15. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/app.py +0 -0
  16. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/classifiers/__init__.py +0 -0
  17. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/classifiers/conductor.py +0 -0
  18. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/classifiers/droid.py +0 -0
  19. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/classifiers/native.py +0 -0
  20. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/cli.py +0 -0
  21. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/command.py +0 -0
  22. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/conditions.py +0 -0
  23. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/context.py +0 -0
  24. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/decisions.py +0 -0
  25. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/dispatch.py +0 -0
  26. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/events.py +0 -0
  27. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/file.py +0 -0
  28. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/llm/__init__.py +0 -0
  29. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/loader.py +0 -0
  30. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/log.py +0 -0
  31. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/packs/__init__.py +0 -0
  32. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/packs/general/capt-hook.toml +0 -0
  33. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/packs/general/commands.py +0 -0
  34. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/packs/general/plans.py +0 -0
  35. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/packs/general/prompts.py +0 -0
  36. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/packs/general/review.py +0 -0
  37. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/packs/general/stewardship.py +0 -0
  38. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/packs/general/tasks.py +0 -0
  39. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/packs/manager.py +0 -0
  40. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/packs/python/capt-hook.toml +0 -0
  41. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/packs/python/style.py +0 -0
  42. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/packs/python/testing.py +0 -0
  43. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/packs/python/toolchain.py +0 -0
  44. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/primitives/__init__.py +0 -0
  45. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/primitives/commands.py +0 -0
  46. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/primitives/lint.py +0 -0
  47. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/primitives/llm.py +0 -0
  48. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/primitives/nudge.py +0 -0
  49. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/primitives/workflow.py +0 -0
  50. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/prompt.py +0 -0
  51. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/py.typed +0 -0
  52. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/review/__init__.py +0 -0
  53. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/review/dashboard.py +0 -0
  54. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/review/fix.py +0 -0
  55. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/review/formats.py +0 -0
  56. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/review/judge.py +0 -0
  57. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/review/pipeline.py +0 -0
  58. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/review/repo.py +0 -0
  59. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/review/settings.py +0 -0
  60. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/review/sync.py +0 -0
  61. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/session.py +0 -0
  62. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/settings.py +0 -0
  63. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/signals/__init__.py +0 -0
  64. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/signals/nlp.py +0 -0
  65. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
  66. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
  67. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
  68. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
  69. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
  70. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
  71. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
  72. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
  73. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
  74. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
  75. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
  76. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
  77. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
  78. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/state.py +0 -0
  79. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/style/__init__.py +0 -0
  80. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/style/matchers.py +0 -0
  81. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/style/scope.py +0 -0
  82. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/style/types.py +0 -0
  83. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/tasks.py +0 -0
  84. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
  85. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/testing/__init__.py +0 -0
  86. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/testing/helpers.py +0 -0
  87. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/testing/session_cache.py +0 -0
  88. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/testing/types.py +0 -0
  89. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/tests/__init__.py +0 -0
  90. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/tests/helpers.py +0 -0
  91. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/types.py +0 -0
  92. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/util/__init__.py +0 -0
  93. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/util/model_cache.py +0 -0
  94. {capt_hook-3.7.0 → capt_hook-3.9.0}/captain_hook/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capt-hook
3
- Version: 3.7.0
3
+ Version: 3.9.0
4
4
  Summary: Declarative hook framework for Claude Code
5
5
  Keywords: claude,claude-code,hooks,llm,agents,guardrails,cli
6
6
  Author: Yasyf Mohamedali
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
16
16
  Classifier: Topic :: Software Development :: Quality Assurance
17
17
  Classifier: Topic :: Software Development :: Testing
18
18
  Classifier: Typing :: Typed
19
- Requires-Dist: cc-transcript>=3.2,<4
19
+ Requires-Dist: cc-transcript>=4,<5
20
20
  Requires-Dist: pydantic>=2.0
21
21
  Requires-Dist: pydantic-settings>=2.0
22
22
  Requires-Dist: tree-sitter>=0.24
@@ -30,7 +30,7 @@ Requires-Dist: wn>=1.1.0
30
30
  Requires-Dist: lazy-object-proxy>=1.12.0
31
31
  Requires-Dist: filelock>=3
32
32
  Requires-Dist: loguru>=0.7.3
33
- Requires-Dist: spawnllm>=0.1.3
33
+ Requires-Dist: spawnllm>=0.2.0
34
34
  Requires-Dist: pytest>=8.0 ; extra == 'dev'
35
35
  Requires-Dist: pytest-asyncio>=0.24 ; extra == 'dev'
36
36
  Requires-Dist: pyright>=1.1 ; extra == 'dev'
@@ -12,7 +12,9 @@ from captain_hook import Allow, FilePath, Input, Tool, UsedSkill, Warn, nudge
12
12
  nudge(
13
13
  "You're editing documentation. Consult the writing-docs skill first for the "
14
14
  "Diataxis modes, voice rules, and code-sample rules, then run "
15
- "`slop-cop check <file> --lang=markdown` to catch prose tells before you finish.",
15
+ "`slop-cop check <file> --lang=markdown` to catch prose tells before you finish. "
16
+ "slop-cop is a Go binary — if it's not on PATH, run the `/slop-cop-check` skill "
17
+ "(it installs it), never `uvx slop-cop`.",
16
18
  only_if=[Tool("Write|Edit"), FilePath("**/*.md", "**/*.qmd", "docs/**", "README.md")],
17
19
  skip_if=[UsedSkill("writing-docs|writing-docs:writing-docs")],
18
20
  max_fires=1,
@@ -0,0 +1,4 @@
1
+ name = "go"
2
+ version = "0.1.0"
3
+ description = "Go-specific guards: a go-test-before-commit gate, gofumpt/golangci-lint toolchain guards, and a go.mod nudge on module-resolution errors."
4
+ hooks = "."
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ from captain_hook import (
4
+ Allow,
5
+ BaseHookEvent,
6
+ Block,
7
+ CustomCondition,
8
+ Event,
9
+ FilePath,
10
+ Input,
11
+ RanCommand,
12
+ Tool,
13
+ Warn,
14
+ gate,
15
+ nudge,
16
+ )
17
+ from captain_hook.types import Command as CommandCondition
18
+
19
+ nudge(
20
+ """
21
+ When a test fails, isolate the minimal failing case before retrying. Use a
22
+ `-run TestName/case` regex or a single package path. Broad `go test ./...`
23
+ re-runs after a failure waste cycles and hide the real breakage.
24
+ """,
25
+ only_if=[Tool("Edit|Write"), FilePath("*_test.go")],
26
+ )
27
+
28
+
29
+ class UserSaid(CustomCondition):
30
+ """Matches when the user's messages contain any of the given keywords."""
31
+
32
+ def __init__(self, *keywords: str) -> None:
33
+ self.keywords = keywords
34
+
35
+ def check(self, evt: BaseHookEvent) -> bool:
36
+ return evt.ctx.t.user_said(*self.keywords)
37
+
38
+
39
+ class AllEditsUnder(CustomCondition):
40
+ """Matches when every edit this session is under one of the given path prefixes."""
41
+
42
+ def __init__(self, *prefixes: str) -> None:
43
+ self.prefixes = prefixes
44
+
45
+ def check(self, evt: BaseHookEvent) -> bool:
46
+ return bool(files := evt.ctx.t.edited_files) and all(f.under(*self.prefixes) for f in files)
47
+
48
+
49
+ class CommitsGo(CustomCondition):
50
+ """Matches when the git command explicitly names a Go path."""
51
+
52
+ def check(self, evt: BaseHookEvent) -> bool:
53
+ return bool(cl := evt.command_line) and ".go" in str(cl.primary)
54
+
55
+
56
+ gate(
57
+ "No `go test` execution found. Run tests before committing Go changes.",
58
+ only_if=[Tool("Bash"), CommandCondition(r"git\s+commit"), CommitsGo()],
59
+ skip_if=[
60
+ RanCommand(r"go test"),
61
+ UserSaid("commit", "just commit"),
62
+ AllEditsUnder("docs/", ".claude/", ".github/"),
63
+ ],
64
+ events=Event.PreToolUse,
65
+ tests={
66
+ Input(command="git status"): Allow(),
67
+ Input(command="git commit internal/cli/root.go"): Block(),
68
+ },
69
+ )
70
+
71
+
72
+ nudge(
73
+ "No `go test` execution detected in this session. If you changed Go files, run tests "
74
+ "before committing. If this is a docs/config-only change, proceed.",
75
+ only_if=[Tool("Bash"), CommandCondition(r"git\s+commit")],
76
+ skip_if=[
77
+ RanCommand(r"go test"),
78
+ UserSaid("commit", "just commit"),
79
+ AllEditsUnder("docs/", ".claude/", ".github/"),
80
+ CommitsGo(),
81
+ ],
82
+ events=Event.PreToolUse,
83
+ tests={
84
+ Input(command="git status"): Allow(),
85
+ Input(command="git commit -m wip"): Warn(),
86
+ },
87
+ )
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from captain_hook import Allow, Block, Event, Input, Tool, block_command, nudge
6
+ from captain_hook.events import PostToolUseFailureEvent
7
+
8
+ block_command(
9
+ r"^gofumpt\b",
10
+ reason="Do not run gofumpt manually — formatting is applied by the commit hook (golangci-lint fmt)",
11
+ hint="See AGENTS.md § Mechanical Linting. The prek hook formats on commit; `task fmt` runs it deliberately.",
12
+ tests={
13
+ Input(command="gofumpt -w ."): Block(),
14
+ Input(command="go build ./..."): Allow(),
15
+ },
16
+ )
17
+
18
+ block_command(
19
+ r"^golangci-lint\b",
20
+ reason="Do not run golangci-lint manually — CI and the commit hook own linting",
21
+ hint="See AGENTS.md § Mechanical Linting. Only fix issues requiring human judgment.",
22
+ tests={
23
+ Input(command="golangci-lint run"): Block(),
24
+ Input(command="golangci-lint fmt"): Block(),
25
+ Input(command="go vet ./..."): Allow(),
26
+ Input(command="uvx prek run --all-files"): Allow(),
27
+ },
28
+ )
29
+
30
+ nudge(
31
+ "MISSING DEPENDENCY: run `go mod tidy` (or `go get <module>`) to resolve it. "
32
+ "Do NOT make the import lazy, delete the importing code, or vendor by hand.",
33
+ events=Event.PostToolUseFailure,
34
+ only_if=[Tool("Bash")],
35
+ when=lambda evt: (
36
+ isinstance(evt, PostToolUseFailureEvent)
37
+ and bool(
38
+ re.search(
39
+ r"no required module provides package|missing go\.sum entry|"
40
+ r"cannot find module|updates to go\.mod needed",
41
+ evt.error,
42
+ )
43
+ )
44
+ ),
45
+ max_fires=2,
46
+ )
@@ -21,6 +21,8 @@ import click
21
21
  if TYPE_CHECKING:
22
22
  from typing import Any
23
23
 
24
+ from cc_transcript.corrections import Correction
25
+
24
26
  from captain_hook.cli import CliState
25
27
  from captain_hook.review.judge import JudgeReport
26
28
  from captain_hook.review.repo import RepoKey
@@ -229,26 +231,52 @@ def list_candidates(state: CliState, repo_: str | None) -> None:
229
231
  click.echo(candidate_line(row))
230
232
 
231
233
 
234
+ def correction_lines(correction: Correction) -> tuple[str, ...]:
235
+ match correction.correction_origin:
236
+ case "session" | "git":
237
+ return (
238
+ f" correction ({correction.correction_origin}):",
239
+ f" - {correction.correction_old}",
240
+ f" + {correction.correction_new}",
241
+ )
242
+ case _ if correction.correction_text:
243
+ return (f" correction note: {correction.correction_text}",)
244
+ case _:
245
+ return ()
246
+
247
+
248
+ def correction_block(correction: Correction) -> str:
249
+ return "\n".join(
250
+ (
251
+ f"- {correction.incorrect_file} (session {correction.session_id}):",
252
+ f" - {correction.incorrect_old}",
253
+ f" + {correction.incorrect_new}",
254
+ *correction_lines(correction),
255
+ )
256
+ )
257
+
258
+
232
259
  @review.command()
233
260
  @click.argument("candidate_id", type=int)
234
261
  def show(candidate_id: int) -> None:
235
- """Show one candidate's row and its threshold status."""
262
+ """Show one candidate's row, its threshold status, and the shared ledger's faulted edits."""
236
263
  from captain_hook.review.judge import REVIEW_PROMPT_VERSION
237
264
  from captain_hook.review.settings import ReviewSettings
238
265
  from captain_hook.review.store import ReviewStore
239
266
 
240
267
  settings = ReviewSettings()
241
268
 
242
- async def go() -> tuple[dict[str, object], ThresholdStatus, bool]:
269
+ async def go() -> tuple[dict[str, object], ThresholdStatus, bool, tuple[Correction, ...]]:
243
270
  async with await ReviewStore.open(settings.db_path) as store:
244
271
  return (
245
272
  await store.candidate(candidate_id),
246
273
  await store.threshold_status(candidate_id, settings=settings, prompt_version=REVIEW_PROMPT_VERSION),
247
274
  await store.eligible(candidate_id, settings=settings, prompt_version=REVIEW_PROMPT_VERSION),
275
+ await store.correction_evidence(candidate_id),
248
276
  )
249
277
 
250
278
  try:
251
- row, status, ok = asyncio.run(go())
279
+ row, status, ok, evidence = asyncio.run(go())
252
280
  except LookupError as exc:
253
281
  raise click.ClickException(str(exc)) from exc
254
282
  for key, value in row.items():
@@ -257,6 +285,10 @@ def show(candidate_id: int) -> None:
257
285
  f"thresholds: sessions={status.sessions} days={status.days} open_prs={status.open_prs} "
258
286
  f"single_observation={status.single_observation} eligible={ok}"
259
287
  )
288
+ if evidence:
289
+ click.echo("correction_evidence:")
290
+ for correction in evidence:
291
+ click.echo(correction_block(correction))
260
292
 
261
293
 
262
294
  @review.command(name="threshold-check")
@@ -30,6 +30,7 @@ from __future__ import annotations
30
30
 
31
31
  from dataclasses import dataclass
32
32
  from itertools import chain
33
+ from pathlib import Path
33
34
  from typing import TYPE_CHECKING
34
35
 
35
36
  from cc_transcript.activity import SessionActivity
@@ -76,7 +77,6 @@ from captain_hook.review.store import CandidateKind
76
77
 
77
78
  if TYPE_CHECKING:
78
79
  from collections.abc import Iterable, Iterator, Mapping, Sequence
79
- from pathlib import Path
80
80
  from typing import Any
81
81
 
82
82
  from cc_transcript.backend import ParsedTranscript
@@ -243,6 +243,39 @@ def transcript_repo(events: Sequence[TranscriptEvent]) -> RepoKey | None:
243
243
  )
244
244
 
245
245
 
246
+ def transcript_cwd(events: Sequence[TranscriptEvent]) -> Path | None:
247
+ return next(
248
+ (Path(meta.cwd) for event in events if (meta := event_meta(event)) is not None if meta.cwd is not None),
249
+ None,
250
+ )
251
+
252
+
253
+ async def record_corrections(
254
+ events: Sequence[TranscriptEvent], kept: Sequence[tuple[MiningSignal, FeedbackCandidate]], *, repo: Path | None
255
+ ) -> None:
256
+ """Grounds each user-correction candidate in the shared code-correction ledger.
257
+
258
+ For every kept user-correction signal (the FIX-mode ``hook_complaint`` is a
259
+ local hook misfire, not a code correction, so it is skipped), harvests the
260
+ edit the feedback faults around its anchor and appends one row to the family
261
+ ledger. Idempotent per anchor: a no-op when cc-pushback already wrote it, so
262
+ captain-hook only fills the ledger for sessions nobody else processed.
263
+ """
264
+ from cc_transcript.corrections import CorrectionLog
265
+ from cc_transcript.extract import extract_correction, usable_backend
266
+
267
+ corrections = [(sig, candidate) for sig, candidate in kept if sig.kind != HOOK_COMPLAINT]
268
+ if not corrections:
269
+ return
270
+ activity = SessionActivity.from_events(corrections[0][0].session_id, events)
271
+ backend = usable_backend()
272
+ log = CorrectionLog.open()
273
+ for sig, candidate in corrections:
274
+ await extract_correction(
275
+ log, activity, candidate.ref, source="captain-hook", feedback=sig.text, repo=repo, backend=backend
276
+ )
277
+
278
+
246
279
  async def ingest(
247
280
  store: ReviewStore, parsed: ParsedTranscript, *, settings: ReviewSettings, repo_key: RepoKey | None
248
281
  ) -> ScanReport:
@@ -275,6 +308,7 @@ async def ingest(
275
308
  await store.record_observation(
276
309
  candidate_id, dedup_key=candidate.dedup_key, session_id=sig.session_id, occurred_at=sig.occurred_at
277
310
  )
311
+ await record_corrections(parsed.events, kept, repo=transcript_cwd(parsed.events))
278
312
  return ScanReport(scanned=1, inserted=inserted)
279
313
 
280
314
 
@@ -28,6 +28,7 @@ if TYPE_CHECKING:
28
28
  from pathlib import Path
29
29
  from typing import Any
30
30
 
31
+ from cc_transcript.corrections import Correction
31
32
  from cc_transcript.ids import SessionId
32
33
  from cc_transcript.mining.candidates import DedupKey
33
34
  from cc_transcript.mining.confidence import Confidence
@@ -105,6 +106,14 @@ WHERE o.candidate_id = ? AND l.accepted = 1 AND l.confidence >= ?
105
106
 
106
107
  OPEN_PRS_QUERY = "SELECT COUNT(*) AS n FROM candidates WHERE repo_key = ? AND status = ? AND pr_opened_at > ?"
107
108
 
109
+ OBSERVATION_ANCHORS_QUERY = """
110
+ SELECT DISTINCT e.session_id, e.event_uuid
111
+ FROM candidate_observations o
112
+ JOIN feedback_events e ON e.dedup_key = o.dedup_key
113
+ WHERE o.candidate_id = ? AND e.session_id IS NOT NULL AND e.event_uuid IS NOT NULL
114
+ ORDER BY o.id
115
+ """
116
+
108
117
  CANDIDATES_QUERY = """
109
118
  SELECT c.*,
110
119
  (SELECT e.text FROM candidate_observations o JOIN feedback_events e ON e.dedup_key = o.dedup_key
@@ -502,6 +511,26 @@ class ReviewStore(VerdictStoreMixin, FeedbackStore):
502
511
  )
503
512
  return str(rows[0]["summary"]) if (rows := [dict(row) async for row in cur]) else None
504
513
 
514
+ async def correction_evidence(self, candidate_id: int) -> tuple[Correction, ...]:
515
+ """Returns the shared-ledger code corrections grounding a candidate's observations.
516
+
517
+ Joins each observation back to its feedback anchor ``(session_id,
518
+ event_uuid)`` and pulls the corrections the family ledger holds for that
519
+ anchor — the offending before/after edit the PR-drafting brain needs. The
520
+ reviewer's own per-session pass writes these rows, so a candidate that
521
+ crossed its thresholds carries its faulted edits.
522
+ """
523
+ from cc_transcript.corrections import CorrectionLog
524
+ from cc_transcript.ids import EventUuid, SessionId
525
+
526
+ cur = await self.store.conn.execute(OBSERVATION_ANCHORS_QUERY, (candidate_id,))
527
+ log = CorrectionLog.open()
528
+ return tuple(
529
+ correction
530
+ for row in [dict(row) async for row in cur]
531
+ for correction in log.for_anchor(SessionId(str(row["session_id"])), EventUuid(str(row["event_uuid"])))
532
+ )
533
+
505
534
  async def candidate_view(
506
535
  self, row: dict[str, object], *, settings: ReviewSettings, prompt_version: int
507
536
  ) -> CandidateView:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "capt-hook"
3
- version = "3.7.0"
3
+ version = "3.9.0"
4
4
  description = "Declarative hook framework for Claude Code"
5
5
  readme = "README.md"
6
6
  license = "PolyForm-Noncommercial-1.0.0"
@@ -20,7 +20,7 @@ classifiers = [
20
20
  ]
21
21
  requires-python = ">=3.13"
22
22
  dependencies = [
23
- "cc-transcript>=3.2,<4",
23
+ "cc-transcript>=4,<5",
24
24
  "pydantic>=2.0",
25
25
  "pydantic-settings>=2.0",
26
26
  "tree-sitter>=0.24",
@@ -34,7 +34,7 @@ dependencies = [
34
34
  "lazy-object-proxy>=1.12.0",
35
35
  "filelock>=3",
36
36
  "loguru>=0.7.3",
37
- "spawnllm>=0.1.3",
37
+ "spawnllm>=0.2.0",
38
38
  ]
39
39
 
40
40
  [project.optional-dependencies]
File without changes
File without changes
File without changes
File without changes
File without changes