/** * User-pinned instructions. * * The problem: CLAUDE.md arrives in message 0, and by the time a long tool loop * has run, position 0 reads as background rather than instruction. The harness * itself works around this for its own rules by re-emitting `` * at the TAIL, where the model reads it last, immediately before generating. * A pin buys that slot for the user's rules. * * Syntax is one line, one pin, wherever the user can type: * * @pxpipe pin be concise, no walls of text * @pxpipe unpin * * The line is identical in a rules file (durable) and in a chat turn (session-only), * so a session pin that works is made permanent by pasting it into the file. What * differs is only WHERE it was found, which is enough to classify it: * * - inside the leading `` run of message 0 → 'file' * - anywhere in the system prompt → 'file' * - in a user's typed text → 'session' * - in any later `` (inlined `@`-mention) → 'session' * * Two file locations because harnesses disagree on where a rules file goes. * Claude Code inlines CLAUDE.md into message 0 behind a `` * envelope; OpenCode puts AGENTS.md in the system prompt with no wrapper and no * label. Scanning only the first shape is what made #155 a no-op under OpenCode. * Both are a file the user can edit, so both earn the durable tier. * * State is re-derived from the transcript on every request — no store, no session * id (the Messages API has none), and rewinding the conversation rewinds the pins. * This works even though we strip the commands from the outbound copy: pxpipe only * rewrites the request going upstream, so the client's own transcript keeps * re-sending them verbatim every turn. */ import type { Message, SystemField } from './types.js'; /** Opening marker of a synthesized pin confirmation. We generate these, so the * match is on bytes we control, not a heuristic that could eat a real reply. */ /** Prefix on every synthesized reply, and the whole test for recognizing one on * the way back in. Shares the `@pxpipe` namespace with the commands so there is * one token to learn and one to match, and unlike `·` it survives any encoding * or copy/paste the transcript is put through. */ export declare const PIN_REPLY_MARK = "@pxpipe "; /** * Which tier a pin lives in. `'file'`, not `'claude.md'`: the same tier holds * whatever the harness inlined, which is CLAUDE.md under Claude Code and * AGENTS.md under OpenCode. (Codex inlines AGENTS.md too, but it speaks the * Responses API, and foldPins has exactly one call site: the Messages path in * transform.ts. Pins are still a no-op there.) * * The tier is named for the origin because the origin is what answers the two * questions a user has: why `unpin` refuses, and what to edit instead. */ export type PinSource = 'file' | 'session'; /** Which command the live turn ended with — it decides what the reply lists. */ export type PinVerb = 'pin' | 'unpin'; export interface Pin { text: string; source: PinSource; /** * Absolute path of the file this line came from, when the harness named it in * the envelope. Carried per pin, not per tier, because one leading run can * inline several files and a header that says `CLAUDE.md` over a line that * came from `AGENTS.md` sends the user to edit the wrong file. */ path?: string; } /** * Fold every pin command in the transcript, oldest to newest. * * @pxpipe unpin 2 remove the pin listed as 2 * @pxpipe unpin use tabs remove by text (exact, else prefix) * @pxpipe unpin all clear every session pin * @pxpipe unpin NOT destructive — prints the list * * Bare `unpin` used to clear everything and then print what survived, which read * as a menu and destroyed the thing the user was still deciding about. Removal now * requires naming a target; the bare form is a query. * * Every form clears SESSION pins only. Clearing CLAUDE.md pins too would mean an * `unpin` typed forty turns ago silently disables the project's rules for the * rest of the session with nothing on screen to explain why; to drop a durable * pin you delete its line from the file, where the change is visible. */ export declare function foldPins(messages: Message[], system?: SystemField): Pin[]; /** * Remove pin commands and their synthesized confirmations from the OUTBOUND copy. * The client keeps its own transcript, so nothing is lost: the next request still * arrives with every command intact and foldPins re-derives the same state. * * A command-only turn is dropped together with the confirmation that answered it — * never one alone, or the user/assistant roles stop alternating. A mixed turn keeps * its prose and loses only the command lines, so the model is not distracted by * configuration in the middle of a request it has to act on. */ export declare function stripPinCommands(messages: Message[]): Message[]; /** * Remove pin commands from the system prompt. Returns `undefined` when nothing * changed, so the caller can leave the original object identity alone. * * Same move as stripFromMessage, including the `cache_control` handoff: system * blocks carry the breakpoint that ends the cacheable prefix (extractSystemText * keys on it), so a block emptied by stripping must pass its marker forward * rather than take the boundary with it. * * Stripping here does rewrite bytes inside the cached prefix. That is the cost * the message path already pays for CLAUDE.md: one cache create on the turn the * strip first applies, then a stable prefix, because the rewrite is a pure * function of the input and the client keeps re-sending the same source lines. */ export declare function stripPinCommandsFromSystem(sys: SystemField | undefined): SystemField | undefined; /** * The block appended at the tail. Sits after every existing `cache_control` * breakpoint, so its bytes are re-read each turn by construction and it can never * cost a cache miss — which is also why formatting drift in the user's source * lines is free here, and why we normalize purely for tidy telemetry. */ export declare function pinBlockText(pins: Pin[]): string; /** * True when appendPinBlock has somewhere to land. * * Pins are MOVED, not copied, so the strip and the append are one operation: an * assistant prefill as the final message (nothing may follow it) or a final user * turn with content we cannot push onto means the block has no home, and * stripping anyway would delete the user's rules from the request entirely. */ export declare function canAppendPinBlock(messages: Message[]): boolean; /** Append the pin block to the final user message. Returns chars added. */ export declare function appendPinBlock(messages: Message[], pins: Pin[]): number; export declare function pinReplyText(pins: Pin[], verb?: PinVerb): string; /** * True when the live turn is nothing but pin commands, so the proxy can answer it * without spending an upstream call. Deliberately narrow: a turn carrying any real * prose, or a tool_result (a tool loop that must not be hijacked mid-flight), is * forwarded normally with the command lines merely stripped. */ export declare function isPinOnlyRequest(messages: Message[] | undefined): boolean; /** * Answer a bare pin turn locally, or undefined to forward normally. * * Fails open on anything unexpected — a malformed body is the upstream's problem * to report, not ours to swallow, and a parse quirk must never cost the user a * turn that had real work in it. */ export declare function pinCommandResponse(bodyIn: Uint8Array): { body: string; contentType: string; } | undefined; /** A local `/v1/messages` response carrying `text`, matching the client's * streaming preference. Zero usage: nothing was billed, and reporting otherwise * would corrupt the dashboard's token accounting. */ export declare function synthesizeReply(text: string, model: string, stream: boolean): { body: string; contentType: string; }; /** Which OpenAI wire schema the reply has to imitate. */ export type OpenAIPinWire = 'chat' | 'responses'; /** `pinCommandResponse` for the OpenAI routes. Codex talks Responses and other * clients talk Chat Completions, so the same command has to be answered in * whichever schema the caller used. */ export declare function pinCommandResponseOpenAI(bodyIn: Uint8Array, wire: OpenAIPinWire): { body: string; contentType: string; } | undefined; /** Local `/v1/chat/completions` reply carrying `text`. Zero usage: nothing was * billed, and reporting otherwise would corrupt the dashboard's accounting. */ export declare function synthesizeChatReply(text: string, model: string, stream: boolean): { body: string; contentType: string; }; /** Local `/v1/responses` reply carrying `text`, in the event order the Responses * API emits: clients read the final item from `response.completed`, but Codex * renders the streamed deltas, so both have to be present. */ export declare function synthesizeResponsesReply(text: string, model: string, stream: boolean): { body: string; contentType: string; }; //# sourceMappingURL=pin.d.ts.map