## Prompt Contexts


## PromptContext


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


Usage

``` python
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

``` python
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

``` python
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

``` python
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

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


Exactly one of [kind](primitives.md#captain_hook.style.matchers.kind)/`pattern` selects the constructs: [kind](primitives.md#captain_hook.style.matchers.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()](prompt-contexts.md#captain_hook.Introduced) renders `<introduced>`; a subclass named `TombstoneComments` renders `<tombstone_comments>`). Subclass and override [keep()](prompt-contexts.md#captain_hook.Introduced.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

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


#### Methods

| Name | Description |
|----|----|
| [keep()](#captain_hook.Introduced.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

``` python
keep(text)
```


## WorkflowScriptSource


Gating context: the pending [Workflow](state-sessions.md#captain_hook.Workflow) call's script source, headed by its model pins.


Usage

``` python
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

``` python
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()](prompt-contexts.md#captain_hook.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.


#### 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

``` python
>>> 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

``` python
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()](prompt-contexts.md#captain_hook.Excerpts.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](#captain_hook.Excerpts.capped) | Whether the budget excluded at least one span. |


##### capped


Whether the budget excluded at least one span.


`capped: bool`


#### Methods

| Name | Description |
|----|----|
| [block()](#captain_hook.Excerpts.block) | Render the excerpts as an `indent`-prefixed block. |


##### block()


Render the excerpts as an `indent`-prefixed block.


Usage

``` python
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

``` python
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

``` python
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](prompt-contexts.md#captain_hook.SyntaxNode)\]\[captain_hook.SyntaxNode\] trees; [matches](events-results.md#captain_hook.Turn.matches) and [introduced](prompt-contexts.md#captain_hook.Edit.introduced) take ast-grep pattern strings (`"print($$$)"`).


#### Parameter Attributes


`old_text: str`  

`new_text: str`  

`lang: str`  


#### Example

``` python
>>> 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()](#captain_hook.Edit.introduced) | Matches of `pattern` present after the edit but absent before it, by whitespace-normalized text. |
| [matches()](#captain_hook.Edit.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

``` python
introduced(pattern)
```


##### matches()


Whether `pattern` matches anywhere in the post-edit source.


Usage

``` python
matches(pattern)
```


## SyntaxNode


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


Usage

``` python
SyntaxNode(raw)
```


#### Parameter Attributes


`raw: SgNode`  


#### Methods

| Name | Description |
|----|----|
| [descendants()](#captain_hook.SyntaxNode.descendants) | Every node below this one, in document order. |


##### descendants()


Every node below this one, in document order.


Usage

``` python
descendants()
```


## Match


A structural match, located by 1-based line to align with [Violation](primitives.md#captain_hook.style.Violation) and changed-line scoping.


Usage

``` python
Match(line, end_line, text)
```


#### Parameter Attributes


`line: int`  

`end_line: int`  

`text: str`
