/** * Ticket holds (crewly#1369 gap 1) — pure rules. * * A ticket is on hold when the owner said to stop: "do not merge PR #1138, do * not ask Steve". Before this module the hold lived only as a Log line, and * nothing read it: days later a janitor notice about the same worktree became * a new ticket ("Decide the fate of OPEN PR #1138") and a "merge?" card to the * owner, who tapped Yes without recognising it. * * A hold is read from (first match wins): * - the `hold` frontmatter field (set through the ticket update path); * - a hold label ({@link TICKET_HOLD_CONSTANTS.LABELS}); * - legacy Log lines: "HOLD: …", "on hold", "do not merge", "不要合并", "owner * said no" — unless a later Log line lifts it ("hold lifted"). * * Held work is matched by what it names: PR numbers, branches and worktrees * (a worktree named `wt-crew257` also names ticket CREW-257). On held work the * only allowed action is {@link TICKET_HOLD_CONSTANTS.ALLOWED_ACTIONS}. * * @module services/project-tickets/ticket-hold */ import type { ProjectTicket } from '../../types/project-ticket.types.js'; /** Where a hold was read from. */ export type TicketHoldSource = 'field' | 'label' | 'log'; /** A ticket's hold. */ export interface TicketHold { /** The reason, in the words that set it */ reason: string; source: TicketHoldSource; /** The ticket this one is held through ("held by CREW-257: …"), when linked */ heldBy?: string; } /** PR numbers, branches, worktrees and ticket ids a text names. */ export interface WorkRefs { prs: number[]; /** Branch names, lower-cased */ branches: string[]; /** Worktree folder names, lower-cased */ worktrees: string[]; /** Ticket ids, upper-cased */ tickets: string[]; } /** The fields of a ticket the hold rules read. */ export type HoldTicketView = Pick & Partial>; /** A held ticket that the work in question references. */ export interface HeldTicketMatch { ticketId: string; title: string; projectPath?: string; hold: TicketHold; /** What matched ("PR #1138", "branch feat/x", "worktree wt-crew257", "ticket CREW-257") */ matchedOn: string[]; } /** * Clip a reason for quoting. * * @param s - Text * @returns One line of at most {@link TICKET_HOLD_CONSTANTS.REASON_MAX_CHARS} */ export declare function clipHoldReason(s: string): string; /** * Whether a Log message puts the ticket on hold. * * @param message - Log message * @returns True for "HOLD: …", "on hold", "do not merge", "不要合并", … */ export declare function isHoldLogMessage(message: string): boolean; /** * Whether a Log message lifts a hold. * * @param message - Log message * @returns True for "hold lifted", "unhold", … */ export declare function isLiftLogMessage(message: string): boolean; /** * A ticket's hold, or null when it is not held. * * @param t - Ticket * @returns The hold * * @example * readTicketHold({ ...t, log: ['2026-10-06T10:00:00Z · crewly-orc · HOLD: PR #1138: do not merge'] }) * // → { reason: 'HOLD: PR #1138: do not merge', source: 'log' } */ export declare function readTicketHold(t: HoldTicketView): TicketHold | null; /** * Ticket id a worktree folder names: `wt-crew257` / `wt-crew-257` → `CREW-257`. * * @param name - Worktree folder name * @returns The id, or null */ export declare function ticketIdOfWorktree(name: string): string | null; /** * PR numbers, branches, worktrees and ticket ids named in a text. * * Conservative: a bare `#123` is not a PR (issues use it too); a PR is * "PR #123", "PR 123", "pull request #123" or a `/pull/123` link. Ticket ids * come only from worktree names (`wt-crew257`), not from free text — a ticket * that merely mentions another one is not the same work. * * @param text - Any text * @returns The refs (deduplicated) */ export declare function extractWorkRefs(text: string): WorkRefs; /** * Merge several refs. * * @param all - Refs * @returns Union */ export declare function mergeWorkRefs(...all: WorkRefs[]): WorkRefs; /** * Refs of a branch and worktree path (the janitor's view of a worktree). * * @param branch - Branch, or null * @param worktreePath - Worktree path * @returns Refs */ export declare function worktreeRefs(branch: string | null, worktreePath: string): WorkRefs; /** * Everything a ticket names: title, description, Log and the frontmatter keys * a human may add (`pr`, `branch`, `worktree`). * * @param t - Ticket * @returns Refs */ export declare function ticketWorkRefs(t: HoldTicketView): WorkRefs; /** * Which held tickets the given refs point at. * * @param refs - What the new work names * @param tickets - Candidate tickets (any status) * @param opts - `excludeId`: the ticket being checked itself * @returns Matches, tickets held in their own right first */ export declare function findHeldMatches(refs: WorkRefs, tickets: readonly HoldTicketView[], opts?: { excludeId?: string; }): HeldTicketMatch[]; /** * The hold reason written on a ticket held through another one. * * @param match - The held ticket it references * @returns `held by CREW-257: ` */ export declare function linkedHoldReason(match: HeldTicketMatch): string; /** * The note put at the top of a held follow-up ticket's description, so the * agent who picks it up reads the hold before anything else. * * @param match - The held ticket * @returns One paragraph */ export declare function heldDescriptionNote(match: HeldTicketMatch): string; /** * Whether a card only asks to close / drop held work. * * @param text - Question plus option labels * @returns True for "Close PR #1138?" with no merge option */ export declare function isCloseOnlyAsk(text: string): boolean; /** * Why a decision card about held work is refused, or null when it may go up. * * @param text - Question, option labels and details * @param matches - Held tickets it references * @returns The refusal (quotes the hold), or null */ export declare function decisionHoldRefusal(text: string, matches: readonly HeldTicketMatch[]): string | null; /** * The line the janitor's unmerged-worktree notice carries for held work. * * @param match - The held ticket * @returns One sentence */ export declare function heldWorktreeNoticeLine(match: HeldTicketMatch): string; //# sourceMappingURL=ticket-hold.d.ts.map