Prompt Contexts

PromptContext

One declarative XML block attached to an LLM primitive’s prompt.

Usage

Source

PromptContext()

content(evt) runs at evaluation time; None or empty content omits the block. When a required context yields empty content, the primitive skips the LLM call entirely — nothing fires and no fire is consumed.

apply_contexts()

Append each context’s block to prompt in order, each clipped to max_len characters.

Usage

Source

apply_contexts(prompt, evt, contexts, *, max_len=2000)

Over-long content is clipped with an explicit …(+Nch) marker, never a silent truncation. A context yielding None or whitespace-only content has its block omitted; when that context is required, returns None instead — the caller must skip the LLM call entirely.

BeforeEdit

The pending edit’s pre-image, as a <before_edit> block.

Usage

Source

BeforeEdit(tag="before_edit", required=False)

Reads ~captain_hook.events.ToolHookEvent.replaced — an Edit’s old text or a MultiEdit’s joined olds at any event, a Write’s current on-disk content only at PreToolUse (after the Write lands, disk holds the new text). The block is omitted whenever the pre-image is unknowable: non-edit events, Writes off PreToolUse, unreadable paths. Attached to every LLM primitive as a default context; required=False because it is ambient enrichment. Pass BeforeEdit(required=True) explicitly to gate a hook on a non-empty pre-image.

Parameter Attributes

tag: str = "before_edit"
required: bool = False

AfterEdit

The pending edit’s new text, as an <after_edit> block.

Usage

Source

AfterEdit(tag="after_edit", required=False)

Reads ~captain_hook.events.BaseHookEvent.content — the text an Edit, Write, MultiEdit, or NotebookEdit is about to land. Attached to every LLM primitive as a default context; required=False because it is ambient enrichment, empty (and omitted) on non-edit events.

Parameter Attributes

tag: str = "after_edit"
required: bool = False

Introduced

Constructs the pending edit newly introduces, as an auto-tagged block.

Usage

Source

Introduced(kind=None, pattern=None, required=True, tag=None)

Exactly one of kind/pattern selects the constructs: kind names tree-sitter node kinds (a bare string or any set — normalized to frozenset; see ~captain_hook.ast_grep.COMMENT_TYPES), pattern is an ast-grep pattern. Extraction diffs the event’s before/after text, so only constructs absent before the edit appear; files without a supported language yield nothing. The pre-image comes from evt.replaced, so hooks covering Writes need events=Event.PreToolUse — at any other event a Write’s pre-image is unknowable, the context yields None, and (being required) the LLM call skips rather than misreporting every construct as introduced.

required defaults to True: a context you attach explicitly IS the evidence — no evidence, no LLM call. tag auto-derives from the class name in snake_case (Introduced() renders <introduced>; a subclass named TombstoneComments renders <tombstone_comments>). Subclass and override keep() to filter which introduced constructs count.

Parameter Attributes

kind: str | Set[str] | None = None
pattern: str | None = None
required: bool = True
tag: str | None = None

Example

>>> llm_nudge("...", contexts=[Introduced(pattern="print($$$)")],
...           events=Event.PreToolUse, only_if=[Tool("Edit", "Write", "MultiEdit")])

Methods

Name Description
keep() Whether an introduced construct’s text belongs in the block — override to filter.
keep()

Whether an introduced construct’s text belongs in the block — override to filter.

Usage

Source

keep(text)

WorkflowScriptSource

Gating context: the pending Workflow call’s script source, headed by its model pins.

Usage

Source

WorkflowScriptSource(tag="workflow_script", required=True)

Resolves the script via ~captain_hook.conditions.workflow_script_source(), excerpts the text around every model: pin into a header capped at PIN_EXCERPT_CAP, and truncates the body past WORKFLOW_SCRIPT_CAP with an explicit marker. Yields None off a Workflow event or an unreadable script.

Parameter Attributes

tag: str = "workflow_script"
required: bool = True

UserMessages

The session’s user prompts as a <user_messages> request/authorization record.

Usage

Source

UserMessages(last=4, per_message=600, tag="user_messages", required=True)

Collects every real user prompt from the transcript — turn-opening text under the native classifier, so meta and hook-injected user events are excluded — then renders the first prompt followed by the most recent last, deduped where they overlap. Each prompt is clipped to per_message characters with an explicit …(+Nch) marker and prefixed [first] or [recent -N] (-1 most recent) so a judge can order them. The first prompt leads so the original ask survives the tail clip apply_contexts() applies. Yields None when the session carries no user prompt; being required by default, that skips the LLM call rather than judging a hook against an empty authorization record. In a delegated lane the detected classifier keeps the sidechain prompts, so the record is the lane’s own brief and the later messages its team sent it, never the orchestrator’s conversation.

Attributes

last: int

How many of the most recent prompts to render after the first.

per_message: int

Character budget each rendered prompt is clipped to.

tag: str

The XML block tag; defaults to user_messages.

required: bool
Whether empty content skips the LLM call; defaults to True.

Example

>>> llm_gate("does this edit stay within what the user asked?", contexts=[UserMessages()])

Excerpts

Verbatim excerpts pulled from a text under a character budget.

Usage

Source

Excerpts(excerpts, quoted, dropped)

excerpts are the kept windows in source order; quoted counts the spans they cover and dropped the spans excluded once the budget filled. Render with block().

Attributes

excerpts: tuple[str, …]

The kept excerpt strings, in source order, without indentation.

quoted: int

How many input spans the kept excerpts cover.

dropped: int
How many input spans the budget excluded.

Attributes

Name Description
capped Whether the budget excluded at least one span.
capped

Whether the budget excluded at least one span.

capped: bool

Methods

Name Description
block() Render the excerpts as an indent-prefixed block.
block()

Render the excerpts as an indent-prefixed block.

Usage

Source

block(noun, *, indent="  ", empty="(none)")

A capped block ends in a … [+N more <noun> not excerpted] marker line; an empty one renders the empty placeholder.

Parameters
noun: str

Plural noun for the dropped-count marker, e.g. "model pins".

indent: str = " "

Prefix applied to every line, the marker and placeholder included.

empty: str = "(none)"
Placeholder rendered when there are no excerpts.

excerpt_around()

Verbatim excerpts of text around each (start, end) character span.

Usage

Source

excerpt_around(
    text, spans, *, before=160, after=60, whole_line_at=200, budget=2000
)

Spans are whole-text character offsets — re.Match.span() is the natural producer, and ast-grep callers can pass node.range().start.index / node.range().end.index. Spans group by line: a line at or under whole_line_at characters is quoted whole in one window; a longer line yields one window per span, clamped to the line as [start - before, end + after] and merged when windows overlap or touch, each bracketed by only where text was actually elided. Excerpts accumulate in source order under a greedy character budget — the first excerpt is always kept, and once the budget fills every later span is dropped.

Parameters

text: str

The source text to excerpt from.

spans: Sequence[tuple[int, int]]

(start, end) character spans into text, in source order.

before: int = 160

Characters of context kept before each span on a long line.

after: int = 60

Characters of context kept after each span on a long line.

whole_line_at: int = 200

Lines at or under this length are quoted whole.

budget: int = 2000
Character budget across the kept excerpts.

COMMENT_TYPES

Tree-sitter node kinds that denote a comment, across every supported grammar.

COMMENT_TYPES: frozenset[str] = GENERATED_COMMENT_TYPES

The union covers every [LANG_GLOBS][captain_hook.langs.LANG_GLOBS] grammar. Markdown (md) is the verified exception with no comment kinds. A Dart block documentation comment has an outer comment with a nested documentation_block_comment; its grammar has no literal documentation_comment kind. Derived at build time from each grammar’s node-types.json: the named kinds whose name contains comment, minus any referenced as a child inside another comment kind (Rust’s doc_comment and its markers, Dart’s nested block). Generation fails loud if any grammar but Markdown yields no comment kinds.

Edit

The pending edit’s before/after source, parsed for structural queries.

Usage

Source

Edit(old_text, new_text, lang)

Reached via [evt.edit][captain_hook.ToolHookEvent.edit]; None there when the event carries no edit or the file’s language has no grammar. old and new are lazily parsed [SyntaxNode][captain_hook.SyntaxNode] trees; matches and introduced take ast-grep pattern strings ("print($$$)").

Parameter Attributes

old_text: str
new_text: str
lang: str

Example

>>> if evt.edit and (added := evt.edit.introduced("print($$$)")):
...     return evt.warn(f"New print() calls: {[m.line for m in added]}")

Methods

Name Description
introduced() Matches of pattern present after the edit but absent before it, by whitespace-normalized text.
matches() Whether pattern matches anywhere in the post-edit source.
introduced()

Matches of pattern present after the edit but absent before it, by whitespace-normalized text.

Usage

Source

introduced(pattern)
matches()

Whether pattern matches anywhere in the post-edit source.

Usage

Source

matches(pattern)

SyntaxNode

One node of a parsed syntax tree — the framework’s face over the ast-grep binding node.

Usage

Source

SyntaxNode(raw)

Parameter Attributes

raw: SgNode

Methods

Name Description
descendants() Every node below this one, in document order.
descendants()

Every node below this one, in document order.

Usage

Source

descendants()

Match

A structural match, located by 1-based line to align with Violation and changed-line scoping.

Usage

Source

Match(line, end_line, text)

Parameter Attributes

line: int
end_line: int
text: str