capt-hook 8.3.0__tar.gz → 8.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-8.3.0 → capt_hook-8.4.0}/PKG-INFO +1 -1
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/general/models.py +3 -3
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/steering/capt-hook.toml +1 -1
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/steering/steering.py +334 -16
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/review/judge.py +23 -1
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/review/scan.py +4 -0
- capt_hook-8.4.0/captain_hook/signals/__init__.py +108 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/types.py +8 -1
- {capt_hook-8.3.0 → capt_hook-8.4.0}/pyproject.toml +1 -1
- capt_hook-8.3.0/captain_hook/signals/__init__.py +0 -81
- {capt_hook-8.3.0 → capt_hook-8.4.0}/LICENSE +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/README.md +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/.claude-plugin/plugin.json +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/__init__.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/__main__.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/app.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/ast_grep.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/classifiers/__init__.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/classifiers/conductor.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/cli.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/conditions.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/context.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/contexts.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/decisions.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/dispatch.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/durable.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/events.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/fields.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/file.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/loader.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/log.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/__init__.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/fixes/__init__.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/fixes/capt-hook.toml +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/fixes/teammate_permissions.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/general/__init__.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/general/_lib.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/general/capt-hook.toml +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/general/commands.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/general/comments.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/general/detours.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/general/docs.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/general/plans.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/general/prompts.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/general/review.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/general/tasks.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/general/tombstones.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/go/__init__.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/go/capt-hook.toml +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/go/testing.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/go/toolchain.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/manager.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/python/__init__.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/python/capt-hook.toml +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/python/style.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/python/testing.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/python/toolchain.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/packs/steering/__init__.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/primitives/__init__.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/primitives/commands.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/primitives/llm.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/primitives/permissions.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/primitives/rewrite.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/primitives/workflow.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/prompt.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/py.typed +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/review/__init__.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/review/cli.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/review/dashboard.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/review/fix.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/review/formats.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/review/pipeline.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/review/repo.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/review/settings.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/review/store.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/review/sync.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/session.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/settings.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/signals/nlp.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/state.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/style/__init__.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/style/ast_grep.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/style/matchers.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/style/scope.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/style/types.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/tasks.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/testing/__init__.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/testing/helpers.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/testing/types.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/util/__init__.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/util/automode.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/util/fs.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/util/http.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/util/model_cache.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/util/proc.py +0 -0
- {capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/util/text.py +0 -0
|
@@ -770,9 +770,9 @@ An unpinned security audit inherits fable; security review/audit is the codex-wr
|
|
|
770
770
|
script="agent(`Synthesize the confirmed review findings and decide which to fix`)",
|
|
771
771
|
llm={"fire": False},
|
|
772
772
|
): Allow(),
|
|
773
|
-
Input(
|
|
774
|
-
|
|
775
|
-
)
|
|
773
|
+
Input(script="agent(`Audit the login flow for auth bypass and injection; return findings as JSON`)"): Warn(
|
|
774
|
+
pattern="gpt-5.5"
|
|
775
|
+
),
|
|
776
776
|
Input(script="agent('Verify the CLI renders the last page correctly')"): Allow(),
|
|
777
777
|
},
|
|
778
778
|
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name = "steering"
|
|
2
|
-
version = "0.
|
|
2
|
+
version = "0.5.0"
|
|
3
3
|
description = "Steer the agent's judgment on what's worth fixing: prefer fundamental, first-principles fixes over band-aids, don't dismiss a real pre-existing issue, don't chase trivial type noise, and don't defer the requested fix by swapping in docs or a follow-up nobody asked for."
|
|
4
4
|
hooks = "."
|
|
5
5
|
nlp = true
|
|
@@ -358,17 +358,21 @@ decided it) in `reasoning`.""",
|
|
|
358
358
|
|
|
359
359
|
|
|
360
360
|
llm_gate(
|
|
361
|
-
"""You are a senior engineer. Another engineer ("the agent") is
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
361
|
+
"""You are a senior engineer. Another engineer ("the agent") is ending its turn or has just
|
|
362
|
+
produced deferral-shaped content mid-work (a thinking block, a review finding, a task or todo it
|
|
363
|
+
filed). You are running in agent mode in the project's working directory, with read tools. Your
|
|
364
|
+
one job: decide whether the agent DELIVERED the fix the user asked for, or silently DOWNGRADED
|
|
365
|
+
the deliverable while the real fix stays undone.
|
|
365
366
|
|
|
366
367
|
Read first, judge second:
|
|
367
368
|
- The session transcript is rendered above inside `<transcript path="...">`; long content is
|
|
368
369
|
clipped (you'll see `…(+Nch)` markers). Read the FULL exchange from that path (prefer
|
|
369
370
|
`cc-transcript show`/`grep`; else read the file). You need: (1) what the user actually asked
|
|
370
371
|
for (including any later approval of a reduced scope), (2) what the agent actually changed
|
|
371
|
-
this turn, and (3) the agent's
|
|
372
|
+
this turn, and (3) the agent's justification (the flagged lines are in `<context>`). On a
|
|
373
|
+
mid-turn firing the flagged content may be deliberation in flight — check whether the agent
|
|
374
|
+
has COMMITTED to the downgrade (substitute edits landed, a softer task filed as the plan of
|
|
375
|
+
record, the real fix abandoned), not merely weighed it.
|
|
372
376
|
- Then inspect the working directory enough to confirm whether the requested fix was made or
|
|
373
377
|
only a softer substitute (docs, help text, error copy) shipped.
|
|
374
378
|
|
|
@@ -391,21 +395,28 @@ how to proceed before substituting anything; the softer deliverable IS the task
|
|
|
391
395
|
requested; the user approved a plan or message that explicitly named the smaller deliverable (an
|
|
392
396
|
approved ExitPlanMode plan counts as the go-ahead); a genuine hard blockage the agent reported
|
|
393
397
|
plainly (no access to the other repo, missing credentials, an API that does not exist); the
|
|
394
|
-
agent shipped the real fix AND improved docs alongside it
|
|
398
|
+
agent shipped the real fix AND improved docs alongside it; deliberation that names the softer
|
|
399
|
+
option in order to reject it and commits to the real fix; a finding or quoted text reporting
|
|
400
|
+
the codebase's or someone else's existing deferral debt or a genuine upstream bug — reporting
|
|
401
|
+
is not deferring, UNLESS the same content prescribes the downgrade as the remediation (e.g.
|
|
402
|
+
"fix requires a release, so document the constraint instead"); a task recording a genuine
|
|
403
|
+
environmental constraint that replaces no requested fix.
|
|
395
404
|
|
|
396
405
|
Do NOT fire when: the user explicitly asked for the docs/help-text/error-copy change; the turn
|
|
397
406
|
ends in a question to the user about the blocker (asking is the sanctioned escape hatch, not
|
|
398
407
|
laziness); the deferral language refers to genuinely optional extras after the requested fix
|
|
399
|
-
landed;
|
|
400
|
-
papered
|
|
408
|
+
landed; the flagged content is mid-turn deliberation where no substitute has been executed yet;
|
|
409
|
+
or the blockage is real, outside the agent's reach, and clearly reported rather than papered
|
|
410
|
+
over.
|
|
401
411
|
|
|
402
412
|
When uncertain, return block=false. A missed deferral costs one nag; a false alarm on an honest
|
|
403
413
|
stop teaches the agent to ignore this gate. Fire only when a specific tell is clearly present
|
|
404
414
|
and no "do not fire" condition applies. Put your reasoning (under 60 words, ending with the one
|
|
405
415
|
tell that decided it) in `reasoning`.""",
|
|
406
416
|
message=lambda r: (
|
|
407
|
-
"You appear to be deferring the real fix —
|
|
408
|
-
|
|
417
|
+
"You appear to be deferring the real fix — whether closing the turn or still mid-work, "
|
|
418
|
+
"you have declared it out of reach and substituted a softer deliverable (or filed one "
|
|
419
|
+
f"as the plan of record) without asking. Why: {r.reasoning} "
|
|
409
420
|
"Do the fix the user asked for: a release, version bump, or cross-repo change is "
|
|
410
421
|
"routine work here, not a blocker — plan it and do it. If you are genuinely blocked, "
|
|
411
422
|
"stop and ask the user how to proceed instead of substituting docs, help text, or a "
|
|
@@ -445,14 +456,14 @@ tell that decided it) in `reasoning`.""",
|
|
|
445
456
|
Signal(
|
|
446
457
|
pattern=(
|
|
447
458
|
r"(?i)\b(?:improv|updat|expand|clarif|document|add)\w*\s+(?:the\s+|this\s+|that\s+|a\s+)?"
|
|
448
|
-
r"(?:documentation|docs\b|help\s+text|readme|error\s+(?:message|copy|text)
|
|
459
|
+
r"(?:documentation|docs\b|help\s+text|readme|error\s+(?:message|copy|text))"
|
|
449
460
|
),
|
|
450
461
|
weight=1,
|
|
451
462
|
),
|
|
452
463
|
Signal(
|
|
453
464
|
pattern=(
|
|
454
465
|
r"(?i)\b(?:document\w*|not(?:e|es|ed|ing)|record\w*)\s+(?:the\s+|this\s+|that\s+|a\s+|its\s+)?"
|
|
455
|
-
r"(?:limitation|caveat|constraint|shortcoming|known\s+issue|behavior)"
|
|
466
|
+
r"(?:[\w'-]+\s+){0,2}?(?:limitation|caveat|constraint|shortcoming|known\s+issue|behavior)"
|
|
456
467
|
),
|
|
457
468
|
weight=2,
|
|
458
469
|
),
|
|
@@ -460,7 +471,7 @@ tell that decided it) in `reasoning`.""",
|
|
|
460
471
|
pattern=(
|
|
461
472
|
r"(?i)(?:(?:left|leaves?|leaving)\s+(?:the\s+|it\s+|this\s+|that\s+)?"
|
|
462
473
|
r"(?:code|implementation|behavior|bug|issue|it)?\s*(?:as-?is|untouched|unchanged|in\s+place)"
|
|
463
|
-
r"|still\s+(?:present|broken|unfixed|unaddressed|reproduc\w+)"
|
|
474
|
+
r"|still\s+(?:present|broken|unfixed|unaddressed|there\b|reproduc\w+)"
|
|
464
475
|
r"|remains?\s+(?:broken|unfixed|in\s+place))"
|
|
465
476
|
),
|
|
466
477
|
weight=2,
|
|
@@ -474,7 +485,7 @@ tell that decided it) in `reasoning`.""",
|
|
|
474
485
|
),
|
|
475
486
|
Signal(
|
|
476
487
|
pattern=(
|
|
477
|
-
r"(?i)(?:
|
|
488
|
+
r"(?i)(?:workaround|stop-?gap|interim\s+(?:fix|solution|measure)|band-?aid"
|
|
478
489
|
r"|(?:as\s+a\s+)?temporary\s+(?:measure|fix|solution|patch|guard)"
|
|
479
490
|
r"|in\s+the\s+meantime|for\s+the\s+time\s+being)"
|
|
480
491
|
),
|
|
@@ -483,7 +494,8 @@ tell that decided it) in `reasoning`.""",
|
|
|
483
494
|
Signal(pattern=r"(?i)\bfor\s+now\b", weight=1),
|
|
484
495
|
Signal(
|
|
485
496
|
pattern=(
|
|
486
|
-
r"(?i)(?:follow-?up\s+(?:pr|work|issue|task|change)|
|
|
497
|
+
r"(?i)(?:follow-?up\s+(?:pr|work|issue|task|change)|(?:in|as)\s+a\s+follow-?up\b"
|
|
498
|
+
r"|future\s+(?:pr|work|release)"
|
|
487
499
|
r"|file\s+(?:an?\s+)?issue|(?:separate|later|subsequent)\s+(?:pr|patch|pass|iteration)|backlog"
|
|
488
500
|
r"|out\s+of\s+scope\s+for\s+this)"
|
|
489
501
|
),
|
|
@@ -502,8 +514,20 @@ tell that decided it) in `reasoning`.""",
|
|
|
502
514
|
r"|so\s+(?:it|this|that|the\s+\w+)\s+(?:no\s+longer|doesn['’]t|does\s+not|won['’]t)\s+"
|
|
503
515
|
r"(?:crash|die|fail|error)\w*)"
|
|
504
516
|
),
|
|
517
|
+
weight=2,
|
|
518
|
+
),
|
|
519
|
+
Signal(
|
|
520
|
+
pattern=(
|
|
521
|
+
r"(?i)\b(?:punt|defer|postpon)\w*\s+(?:the|this|that|it)\b"
|
|
522
|
+
r"|\bskip\w*\s+the\s+(?:real|proper|actual|root)\b"
|
|
523
|
+
),
|
|
524
|
+
weight=2,
|
|
525
|
+
),
|
|
526
|
+
Signal(
|
|
527
|
+
pattern=r"(?i)\brevisit\s+(?:it\s+|this\s+)?later\b|\bcome\s+back\s+to\s+(?:it|this)\b",
|
|
505
528
|
weight=1,
|
|
506
529
|
),
|
|
530
|
+
Signal(pattern=r"(?i)\bmov(?:e[ds]?|ing)\s+on\b", weight=1),
|
|
507
531
|
NlpSignal(
|
|
508
532
|
clauses=[Clause(noun=Phrase.expand("fix"), verb=Phrase("defer", "postpone", "punt"))],
|
|
509
533
|
weight=2,
|
|
@@ -523,8 +547,9 @@ tell that decided it) in `reasoning`.""",
|
|
|
523
547
|
),
|
|
524
548
|
],
|
|
525
549
|
threshold=3,
|
|
526
|
-
window=
|
|
550
|
+
window="turn",
|
|
527
551
|
),
|
|
552
|
+
events=Event.PostToolUse | Event.Stop | Event.SubagentStop,
|
|
528
553
|
tests={
|
|
529
554
|
Input(
|
|
530
555
|
transcript=[
|
|
@@ -727,5 +752,298 @@ tell that decided it) in `reasoning`.""",
|
|
|
727
752
|
}
|
|
728
753
|
]
|
|
729
754
|
): Allow(),
|
|
755
|
+
Input(
|
|
756
|
+
transcript=[
|
|
757
|
+
{
|
|
758
|
+
"type": "assistant",
|
|
759
|
+
"message": {
|
|
760
|
+
"content": [
|
|
761
|
+
{
|
|
762
|
+
"type": "tool_use",
|
|
763
|
+
"id": "tu_findings",
|
|
764
|
+
"name": "ReportFindings",
|
|
765
|
+
"input": {
|
|
766
|
+
"findings": [
|
|
767
|
+
{
|
|
768
|
+
"file": "internal/daemon/daemon.go",
|
|
769
|
+
"line": 105,
|
|
770
|
+
"summary": (
|
|
771
|
+
"OpClose --stale and OpList are documented as cross-repo "
|
|
772
|
+
"commands, but cc-interact's dispatch runs ScopeResolve "
|
|
773
|
+
"(vcs.Root) on the caller's cwd for every domain op with a "
|
|
774
|
+
"hardcoded exemption set, so both error when invoked "
|
|
775
|
+
"outside a git/jj repo."
|
|
776
|
+
),
|
|
777
|
+
"failure_scenario": (
|
|
778
|
+
"User cd ~ && cc-review list → error from vcs.Root instead "
|
|
779
|
+
"of the cross-repo listing; fix requires a cc-interact "
|
|
780
|
+
"release, so cc-review documents the run-inside-a-repo "
|
|
781
|
+
"constraint instead."
|
|
782
|
+
),
|
|
783
|
+
"verdict": "CONFIRMED",
|
|
784
|
+
}
|
|
785
|
+
]
|
|
786
|
+
},
|
|
787
|
+
}
|
|
788
|
+
]
|
|
789
|
+
},
|
|
790
|
+
}
|
|
791
|
+
]
|
|
792
|
+
): Block(),
|
|
793
|
+
Input(
|
|
794
|
+
transcript=[
|
|
795
|
+
{
|
|
796
|
+
"type": "assistant",
|
|
797
|
+
"message": {
|
|
798
|
+
"content": [
|
|
799
|
+
{
|
|
800
|
+
"type": "thinking",
|
|
801
|
+
"thinking": (
|
|
802
|
+
"I'm seeing that cc-interact has hardcoded exemptions for certain "
|
|
803
|
+
"operations, which means any domain operation without an exemption "
|
|
804
|
+
"gets an error when the scope can't be resolved. The issue is that "
|
|
805
|
+
"`cc-review list` and `close --stale` commands don't have consumer "
|
|
806
|
+
"hooks to exempt them, so running these from outside a repo directory "
|
|
807
|
+
"triggers a vcs.Root error. Since fixing this requires a cc-interact "
|
|
808
|
+
"release, the practical solution is to improve the documentation and "
|
|
809
|
+
"help text to guide users on where to run these commands from."
|
|
810
|
+
),
|
|
811
|
+
}
|
|
812
|
+
]
|
|
813
|
+
},
|
|
814
|
+
}
|
|
815
|
+
]
|
|
816
|
+
): Block(),
|
|
817
|
+
Input(
|
|
818
|
+
transcript=[
|
|
819
|
+
{
|
|
820
|
+
"type": "assistant",
|
|
821
|
+
"message": {
|
|
822
|
+
"content": [
|
|
823
|
+
{
|
|
824
|
+
"type": "text",
|
|
825
|
+
"text": (
|
|
826
|
+
"Since fixing this requires a cc-interact release, the practical "
|
|
827
|
+
"solution is to document the limitation in the help text."
|
|
828
|
+
),
|
|
829
|
+
}
|
|
830
|
+
]
|
|
831
|
+
},
|
|
832
|
+
},
|
|
833
|
+
*(
|
|
834
|
+
block
|
|
835
|
+
for i in range(6)
|
|
836
|
+
for block in (
|
|
837
|
+
{
|
|
838
|
+
"type": "assistant",
|
|
839
|
+
"message": {
|
|
840
|
+
"content": [
|
|
841
|
+
{
|
|
842
|
+
"type": "tool_use",
|
|
843
|
+
"id": f"tu_{i}",
|
|
844
|
+
"name": "Read",
|
|
845
|
+
"input": {"file_path": f"/tmp/f{i}.py"},
|
|
846
|
+
}
|
|
847
|
+
]
|
|
848
|
+
},
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
"type": "user",
|
|
852
|
+
"message": {
|
|
853
|
+
"content": [{"type": "tool_result", "tool_use_id": f"tu_{i}", "content": "ok"}]
|
|
854
|
+
},
|
|
855
|
+
},
|
|
856
|
+
)
|
|
857
|
+
),
|
|
858
|
+
]
|
|
859
|
+
): Block(),
|
|
860
|
+
Input(
|
|
861
|
+
transcript=[
|
|
862
|
+
{
|
|
863
|
+
"type": "assistant",
|
|
864
|
+
"message": {
|
|
865
|
+
"content": [
|
|
866
|
+
{
|
|
867
|
+
"type": "tool_use",
|
|
868
|
+
"id": "tu_findings",
|
|
869
|
+
"name": "ReportFindings",
|
|
870
|
+
"input": {
|
|
871
|
+
"findings": [
|
|
872
|
+
{
|
|
873
|
+
"file": "captain_hook/signals/__init__.py",
|
|
874
|
+
"line": 46,
|
|
875
|
+
"summary": (
|
|
876
|
+
"Off-by-one in windowed() drops the final event when stop "
|
|
877
|
+
"lands on a turn boundary."
|
|
878
|
+
),
|
|
879
|
+
"failure_scenario": (
|
|
880
|
+
"Session.recent(1) on a three-event transcript returns an "
|
|
881
|
+
"empty window, so signal scoring sees no text."
|
|
882
|
+
),
|
|
883
|
+
"verdict": "CONFIRMED",
|
|
884
|
+
}
|
|
885
|
+
]
|
|
886
|
+
},
|
|
887
|
+
}
|
|
888
|
+
]
|
|
889
|
+
},
|
|
890
|
+
}
|
|
891
|
+
]
|
|
892
|
+
): Allow(),
|
|
893
|
+
Input(
|
|
894
|
+
transcript=[
|
|
895
|
+
{
|
|
896
|
+
"type": "assistant",
|
|
897
|
+
"message": {
|
|
898
|
+
"content": [
|
|
899
|
+
{
|
|
900
|
+
"type": "tool_use",
|
|
901
|
+
"id": "tu_todos",
|
|
902
|
+
"name": "TodoWrite",
|
|
903
|
+
"input": {
|
|
904
|
+
"todos": [
|
|
905
|
+
{
|
|
906
|
+
"content": "Add SSE reconnect test",
|
|
907
|
+
"status": "pending",
|
|
908
|
+
"activeForm": "Adding SSE reconnect test",
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
"content": "Wire the backoff cap into the client config",
|
|
912
|
+
"status": "in_progress",
|
|
913
|
+
"activeForm": "Wiring the backoff cap",
|
|
914
|
+
},
|
|
915
|
+
]
|
|
916
|
+
},
|
|
917
|
+
}
|
|
918
|
+
]
|
|
919
|
+
},
|
|
920
|
+
}
|
|
921
|
+
]
|
|
922
|
+
): Allow(),
|
|
923
|
+
Input(
|
|
924
|
+
transcript=[
|
|
925
|
+
{
|
|
926
|
+
"type": "assistant",
|
|
927
|
+
"message": {
|
|
928
|
+
"content": [
|
|
929
|
+
{
|
|
930
|
+
"type": "tool_use",
|
|
931
|
+
"id": "tu_task_workaround",
|
|
932
|
+
"name": "TaskCreate",
|
|
933
|
+
"input": {
|
|
934
|
+
"subject": "Workaround for the crash",
|
|
935
|
+
"description": (
|
|
936
|
+
"Wrap the call in try/except so the session survives; revisit later."
|
|
937
|
+
),
|
|
938
|
+
},
|
|
939
|
+
}
|
|
940
|
+
]
|
|
941
|
+
},
|
|
942
|
+
}
|
|
943
|
+
]
|
|
944
|
+
): Block(),
|
|
945
|
+
Input(
|
|
946
|
+
transcript=[
|
|
947
|
+
{
|
|
948
|
+
"type": "assistant",
|
|
949
|
+
"message": {
|
|
950
|
+
"content": [
|
|
951
|
+
{
|
|
952
|
+
"type": "tool_use",
|
|
953
|
+
"id": "tu_todo_defer",
|
|
954
|
+
"name": "TodoWrite",
|
|
955
|
+
"input": {
|
|
956
|
+
"todos": [
|
|
957
|
+
{
|
|
958
|
+
"content": "Defer the parser rewrite to a later pass",
|
|
959
|
+
"status": "pending",
|
|
960
|
+
"activeForm": "Deferring the parser rewrite",
|
|
961
|
+
},
|
|
962
|
+
]
|
|
963
|
+
},
|
|
964
|
+
}
|
|
965
|
+
]
|
|
966
|
+
},
|
|
967
|
+
}
|
|
968
|
+
]
|
|
969
|
+
): Block(),
|
|
970
|
+
Input(
|
|
971
|
+
transcript=[
|
|
972
|
+
{
|
|
973
|
+
"type": "assistant",
|
|
974
|
+
"message": {
|
|
975
|
+
"content": [
|
|
976
|
+
{
|
|
977
|
+
"type": "tool_use",
|
|
978
|
+
"id": "tu_task_skip",
|
|
979
|
+
"name": "TaskCreate",
|
|
980
|
+
"input": {
|
|
981
|
+
"subject": "Skip the real fix",
|
|
982
|
+
"description": (
|
|
983
|
+
"Too big for this pass; just note it somewhere and come back to it."
|
|
984
|
+
),
|
|
985
|
+
},
|
|
986
|
+
}
|
|
987
|
+
]
|
|
988
|
+
},
|
|
989
|
+
}
|
|
990
|
+
]
|
|
991
|
+
): Block(),
|
|
992
|
+
Input(
|
|
993
|
+
transcript=[
|
|
994
|
+
{
|
|
995
|
+
"type": "assistant",
|
|
996
|
+
"message": {
|
|
997
|
+
"content": [
|
|
998
|
+
{
|
|
999
|
+
"type": "tool_use",
|
|
1000
|
+
"id": "tu_findings_deadcode",
|
|
1001
|
+
"name": "ReportFindings",
|
|
1002
|
+
"input": {
|
|
1003
|
+
"findings": [
|
|
1004
|
+
{
|
|
1005
|
+
"file": "captain_hook/signals/__init__.py",
|
|
1006
|
+
"line": 90,
|
|
1007
|
+
"summary": (
|
|
1008
|
+
"The `document the limitation` fallback branch in "
|
|
1009
|
+
"cite_message is dead code — no caller reaches it."
|
|
1010
|
+
),
|
|
1011
|
+
"failure_scenario": (
|
|
1012
|
+
"extract_signal_context always returns a non-empty list "
|
|
1013
|
+
"for these patterns, so the else arm never runs."
|
|
1014
|
+
),
|
|
1015
|
+
"verdict": "CONFIRMED",
|
|
1016
|
+
}
|
|
1017
|
+
]
|
|
1018
|
+
},
|
|
1019
|
+
}
|
|
1020
|
+
]
|
|
1021
|
+
},
|
|
1022
|
+
}
|
|
1023
|
+
]
|
|
1024
|
+
): Allow(),
|
|
1025
|
+
Input(
|
|
1026
|
+
transcript=[
|
|
1027
|
+
{
|
|
1028
|
+
"type": "assistant",
|
|
1029
|
+
"message": {
|
|
1030
|
+
"content": [
|
|
1031
|
+
{
|
|
1032
|
+
"type": "tool_use",
|
|
1033
|
+
"id": "tu_task_spacy_docs",
|
|
1034
|
+
"name": "TaskCreate",
|
|
1035
|
+
"input": {
|
|
1036
|
+
"subject": "Document the limitation",
|
|
1037
|
+
"description": (
|
|
1038
|
+
"Note in the README that NLP scoring needs the spaCy "
|
|
1039
|
+
"en_core_web_sm model provisioned."
|
|
1040
|
+
),
|
|
1041
|
+
},
|
|
1042
|
+
}
|
|
1043
|
+
]
|
|
1044
|
+
},
|
|
1045
|
+
}
|
|
1046
|
+
]
|
|
1047
|
+
): Allow(),
|
|
730
1048
|
},
|
|
731
1049
|
)
|
|
@@ -26,6 +26,7 @@ from cc_transcript.judge.llm import resolved_model, structured_judge
|
|
|
26
26
|
from cc_transcript.judge.verdicts import run_verdicts
|
|
27
27
|
from cc_transcript.mining.candidates import DedupKey
|
|
28
28
|
from cc_transcript.mining.confidence import NOISE_FLOOR
|
|
29
|
+
from cc_transcript.mining.sourcekind import QUESTION_ANSWER
|
|
29
30
|
from cc_transcript.render import Budget
|
|
30
31
|
from pydantic import BaseModel, Field
|
|
31
32
|
|
|
@@ -131,6 +132,27 @@ async def render_context(window: ContextWindow) -> tuple[str, Fidelity]:
|
|
|
131
132
|
)
|
|
132
133
|
|
|
133
134
|
|
|
135
|
+
def question_answer_block(row: Mapping[str, object]) -> str:
|
|
136
|
+
if str(row["source_kind"]) != QUESTION_ANSWER:
|
|
137
|
+
return ""
|
|
138
|
+
payload: dict[str, object] = json.loads(str(row["payload_json"]))
|
|
139
|
+
match payload["picked_labels"]:
|
|
140
|
+
case []:
|
|
141
|
+
resolved = "The developer picked none of the offered options and wrote a freeform answer."
|
|
142
|
+
case [*labels]:
|
|
143
|
+
choice = "options" if payload["multi_select"] else "option"
|
|
144
|
+
standing = (
|
|
145
|
+
"which is the option the assistant marked (Recommended)"
|
|
146
|
+
if payload["recommended_pick"]
|
|
147
|
+
else "which is not the recommended option"
|
|
148
|
+
)
|
|
149
|
+
resolved = f"The answer resolves to the {choice} {'; '.join(map(str, labels))} — {standing}."
|
|
150
|
+
return (
|
|
151
|
+
f"=== QUESTION THE ASSISTANT ASKED ===\n{payload['question']}\n{resolved}\n"
|
|
152
|
+
"The feedback below is the developer's answer to that question.\n"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
134
156
|
def build_create_prompt(row: Mapping[str, object], context: str) -> str:
|
|
135
157
|
return f"""\
|
|
136
158
|
You are auditing one piece of feedback a developer gave an AI coding assistant
|
|
@@ -170,7 +192,7 @@ Respond with strict JSON matching the schema — no extra keys, no prose.
|
|
|
170
192
|
|
|
171
193
|
[source: {row["source_kind"]}]
|
|
172
194
|
{context}
|
|
173
|
-
=== FEEDBACK TO CLASSIFY ===
|
|
195
|
+
{question_answer_block(row)}=== FEEDBACK TO CLASSIFY ===
|
|
174
196
|
{row["text"]}"""
|
|
175
197
|
|
|
176
198
|
|
|
@@ -150,6 +150,8 @@ def rule_parts(sig: MiningSignal) -> tuple[str, ...]:
|
|
|
150
150
|
str(sig.evidence["line_end"] or ""),
|
|
151
151
|
sig.text,
|
|
152
152
|
)
|
|
153
|
+
case "ask_user_question":
|
|
154
|
+
return ("question_answer", str(sig.evidence["question"] or ""), sig.text)
|
|
153
155
|
case _:
|
|
154
156
|
raise AssertionError(sig.detector)
|
|
155
157
|
|
|
@@ -171,6 +173,8 @@ def payload_of(sig: MiningSignal) -> Mapping[str, Any] | None:
|
|
|
171
173
|
return dict(sig.evidence) or None
|
|
172
174
|
case "review_comment":
|
|
173
175
|
return {key: sig.evidence[key] for key in ("format", "file", "line_start", "line_end")}
|
|
176
|
+
case "ask_user_question":
|
|
177
|
+
return dict(sig.evidence)
|
|
174
178
|
case _:
|
|
175
179
|
raise AssertionError(sig.detector)
|
|
176
180
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
6
|
+
|
|
7
|
+
from cc_transcript.models import AssistantEvent, ThinkingBlock, ToolUseBlock, UserEvent
|
|
8
|
+
|
|
9
|
+
from captain_hook.signals.nlp import NlpSignal
|
|
10
|
+
from captain_hook.types import Event, Signal, Signals
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from captain_hook.events import BaseHookEvent
|
|
14
|
+
|
|
15
|
+
PROSE_TOOLS: dict[str, Callable[[Mapping[str, Any]], list[str]]] = {
|
|
16
|
+
"ReportFindings": lambda inp: [
|
|
17
|
+
" ".join(filter(None, (f.get("summary"), f.get("failure_scenario")))) for f in inp.get("findings", ())
|
|
18
|
+
],
|
|
19
|
+
"TaskCreate": lambda inp: [" ".join(filter(None, (inp.get("subject"), inp.get("description"))))],
|
|
20
|
+
"TaskUpdate": lambda inp: [" ".join(filter(None, (inp.get("subject"), inp.get("description"))))],
|
|
21
|
+
"TodoWrite": lambda inp: [
|
|
22
|
+
" ".join(filter(None, (t.get("content"), t.get("subject")))) for t in inp.get("todos", ())
|
|
23
|
+
],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
TSignalPattern = Signal | NlpSignal
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def score_signals(patterns: Sequence[TSignalPattern], text: str) -> int:
|
|
30
|
+
from captain_hook.signals.nlp import nlp_scan
|
|
31
|
+
|
|
32
|
+
total = 0
|
|
33
|
+
for s in patterns:
|
|
34
|
+
match s:
|
|
35
|
+
case NlpSignal(clauses=clauses) if nlp_scan(clauses, text):
|
|
36
|
+
total += s.weight
|
|
37
|
+
case Signal() if re.search(s.pattern, text, s.flags):
|
|
38
|
+
total += s.weight
|
|
39
|
+
case _:
|
|
40
|
+
pass
|
|
41
|
+
return total
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def extract_signal_context(patterns: Sequence[TSignalPattern], text: str) -> list[str]:
|
|
45
|
+
from captain_hook.signals.nlp import nlp_scan
|
|
46
|
+
|
|
47
|
+
result: list[str] = []
|
|
48
|
+
for s in patterns:
|
|
49
|
+
match s:
|
|
50
|
+
case NlpSignal(clauses=clauses):
|
|
51
|
+
result.extend(nlp_scan(clauses, text))
|
|
52
|
+
case Signal():
|
|
53
|
+
result.extend(line for line in text.splitlines() if re.search(s.pattern, line, s.flags))
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def block_texts(event: UserEvent | AssistantEvent) -> Iterator[str]:
|
|
58
|
+
for block in event.blocks:
|
|
59
|
+
match block:
|
|
60
|
+
case ThinkingBlock(thinking=thinking):
|
|
61
|
+
yield thinking
|
|
62
|
+
case ToolUseBlock(name=name, input=payload) if extract := PROSE_TOOLS.get(name):
|
|
63
|
+
yield from extract(payload)
|
|
64
|
+
case _:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def transcript_texts(evt: BaseHookEvent, window: int | Literal["turn"]) -> list[str]:
|
|
69
|
+
"""Extract prose from recent transcript events for signal scoring.
|
|
70
|
+
|
|
71
|
+
For ``UserPromptSubmit`` events, returns just the user prompt. Otherwise scans
|
|
72
|
+
the last ``window`` events — the whole current turn when ``window`` is
|
|
73
|
+
``"turn"`` — and returns one entry per prose source: each event's ``.text``,
|
|
74
|
+
each thinking block, and the prose fields of prose-carrying tool calls
|
|
75
|
+
(``ReportFindings`` findings, ``TaskCreate``/``TaskUpdate`` subjects and
|
|
76
|
+
descriptions, ``TodoWrite`` todos).
|
|
77
|
+
"""
|
|
78
|
+
if evt.event == Event.UserPromptSubmit and evt.user_prompt:
|
|
79
|
+
return [evt.user_prompt]
|
|
80
|
+
scope = evt.ctx.turn if window == "turn" else evt.ctx.t.recent(window)
|
|
81
|
+
return [
|
|
82
|
+
text
|
|
83
|
+
for event in scope.events
|
|
84
|
+
if isinstance(event, UserEvent | AssistantEvent)
|
|
85
|
+
for text in (event.text, *block_texts(event))
|
|
86
|
+
if text
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def cite_message(sig: Signals, triggering: list[str], message: str) -> str:
|
|
91
|
+
"""Append trigger context to a message when signal matches are found."""
|
|
92
|
+
return (
|
|
93
|
+
f"{message}\n\nTriggered by: {'; '.join(context)}"
|
|
94
|
+
if (context := extract_signal_context(sig.patterns, "\n".join(triggering)))
|
|
95
|
+
else message
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def resolve_signals(signals: Sequence[Signal | NlpSignal] | Signals | None) -> Signals | None:
|
|
100
|
+
"""Normalize signals input into a ``Signals`` bundle, or None.
|
|
101
|
+
|
|
102
|
+
A bare ``list[Signal]`` is wrapped with ``threshold=1`` (any single match triggers).
|
|
103
|
+
"""
|
|
104
|
+
if signals is None:
|
|
105
|
+
return None
|
|
106
|
+
if isinstance(signals, Signals):
|
|
107
|
+
return signals
|
|
108
|
+
return Signals(patterns=list(signals), threshold=1)
|
|
@@ -9,6 +9,7 @@ from typing import (
|
|
|
9
9
|
TYPE_CHECKING,
|
|
10
10
|
Any,
|
|
11
11
|
Generic,
|
|
12
|
+
Literal,
|
|
12
13
|
Protocol,
|
|
13
14
|
TypeVar,
|
|
14
15
|
get_args,
|
|
@@ -657,11 +658,17 @@ class Signals:
|
|
|
657
658
|
When a bare ``list[Signal]`` is passed to a primitive, ``resolve_signals``
|
|
658
659
|
wraps it with ``threshold=1`` — meaning *any* single signal match triggers.
|
|
659
660
|
Pass a higher threshold to require multiple signals to fire together.
|
|
661
|
+
|
|
662
|
+
``window`` bounds how much transcript each scoring pass reads: the last
|
|
663
|
+
``window`` events by default, or the whole current turn with the ``"turn"``
|
|
664
|
+
sentinel. Use ``"turn"`` when the tell can sit hundreds of events before the
|
|
665
|
+
turn ends — a fixed window at ``Stop`` only reaches the last few tool
|
|
666
|
+
exchanges.
|
|
660
667
|
"""
|
|
661
668
|
|
|
662
669
|
patterns: Sequence[Signal | NlpSignal]
|
|
663
670
|
threshold: int
|
|
664
|
-
window: int = 15
|
|
671
|
+
window: int | Literal["turn"] = 15
|
|
665
672
|
|
|
666
673
|
|
|
667
674
|
@dataclass(frozen=True, kw_only=True)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "capt-hook"
|
|
3
3
|
# Inert sentinel — the real version is stamped from the release tag (uv version --frozen).
|
|
4
|
-
version = "8.
|
|
4
|
+
version = "8.4.0"
|
|
5
5
|
description = "Stop repeating yourself to Claude."
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "PolyForm-Noncommercial-1.0.0"
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
from collections.abc import Sequence
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
6
|
-
|
|
7
|
-
from cc_transcript.models import AssistantEvent, UserEvent
|
|
8
|
-
|
|
9
|
-
from captain_hook.signals.nlp import NlpSignal
|
|
10
|
-
from captain_hook.types import Event, Signal, Signals
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from captain_hook.events import BaseHookEvent
|
|
14
|
-
|
|
15
|
-
TSignalPattern = Signal | NlpSignal
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def score_signals(patterns: Sequence[TSignalPattern], text: str) -> int:
|
|
19
|
-
from captain_hook.signals.nlp import nlp_scan
|
|
20
|
-
|
|
21
|
-
total = 0
|
|
22
|
-
for s in patterns:
|
|
23
|
-
match s:
|
|
24
|
-
case NlpSignal(clauses=clauses) if nlp_scan(clauses, text):
|
|
25
|
-
total += s.weight
|
|
26
|
-
case Signal() if re.search(s.pattern, text, s.flags):
|
|
27
|
-
total += s.weight
|
|
28
|
-
case _:
|
|
29
|
-
pass
|
|
30
|
-
return total
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def extract_signal_context(patterns: Sequence[TSignalPattern], text: str) -> list[str]:
|
|
34
|
-
from captain_hook.signals.nlp import nlp_scan
|
|
35
|
-
|
|
36
|
-
result: list[str] = []
|
|
37
|
-
for s in patterns:
|
|
38
|
-
match s:
|
|
39
|
-
case NlpSignal(clauses=clauses):
|
|
40
|
-
result.extend(nlp_scan(clauses, text))
|
|
41
|
-
case Signal():
|
|
42
|
-
result.extend(line for line in text.splitlines() if re.search(s.pattern, line, s.flags))
|
|
43
|
-
return result
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def transcript_texts(evt: BaseHookEvent, window: int) -> list[str]:
|
|
47
|
-
"""Extract text from recent transcript events for signal scoring.
|
|
48
|
-
|
|
49
|
-
For ``UserPromptSubmit`` events, returns just the user prompt.
|
|
50
|
-
Otherwise returns ``.text`` from the last ``window`` events.
|
|
51
|
-
"""
|
|
52
|
-
return (
|
|
53
|
-
[evt.user_prompt]
|
|
54
|
-
if evt.event == Event.UserPromptSubmit and evt.user_prompt
|
|
55
|
-
else [
|
|
56
|
-
text
|
|
57
|
-
for event in evt.ctx.t.recent(window).events
|
|
58
|
-
if isinstance(event, UserEvent | AssistantEvent) and (text := event.text)
|
|
59
|
-
]
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def cite_message(sig: Signals, triggering: list[str], message: str) -> str:
|
|
64
|
-
"""Append trigger context to a message when signal matches are found."""
|
|
65
|
-
return (
|
|
66
|
-
f"{message}\n\nTriggered by: {'; '.join(context)}"
|
|
67
|
-
if (context := extract_signal_context(sig.patterns, "\n".join(triggering)))
|
|
68
|
-
else message
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def resolve_signals(signals: Sequence[Signal | NlpSignal] | Signals | None) -> Signals | None:
|
|
73
|
-
"""Normalize signals input into a ``Signals`` bundle, or None.
|
|
74
|
-
|
|
75
|
-
A bare ``list[Signal]`` is wrapped with ``threshold=1`` (any single match triggers).
|
|
76
|
-
"""
|
|
77
|
-
if signals is None:
|
|
78
|
-
return None
|
|
79
|
-
if isinstance(signals, Signals):
|
|
80
|
-
return signals
|
|
81
|
-
return Signals(patterns=list(signals), threshold=1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md
RENAMED
|
File without changes
|
|
File without changes
|
{capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md
RENAMED
|
File without changes
|
{capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md
RENAMED
|
File without changes
|
{capt_hook-8.3.0 → capt_hook-8.4.0}/captain_hook/skills/scanning-sessions/references/review-cli.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|