/** * Where an agent's answer goes, decided by the WORK it is answering * (specs/2026-10-01-decision-cards.md §6). * * Incident: Atlas posted the output of a scheduled task ("steveswiki 第一次季度 * 回看…") into an unrelated #pro-think-tank thread. The 1.20.170 `reply` sent * every answer to "the thread the agent was last asked in", and a cron run * has no asker — so the last owner thread, however old, won. * * Now each work item carries its own origin and the answer follows it: * * | Work | Destination | * |---|---| * | owner asked (newest, < 2 h) | that conversation / thread (turn origin) | * | ticket work | the ticket's Slack thread (created on first post) | * | trigger / cron / auto work | the trigger's `destination`, else a NEW top-level post in the team channel | * | work an agent delegated on its own initiative | that agent, as an internal message (crewly#1357) | * | no current work | a NEW top-level post in the team channel | * * Pure functions only; the effects live in `work-item-destination.wiring.ts`. * * @module services/orc/work-item-destination */ import type { WorkItem } from '../../types/v2/work-item.types.js'; import type { TurnOrigin } from './orc-reply-route.service.js'; /** The origin a work item carries in `metadata.origin`. */ export type WorkItemOrigin = /** * Asked by the owner in a conversation / Slack thread / app comment. * `receivedBy`: the agent the owner's message reached (it reports the * finished work back); `appComment`: the Crewly App comment it came from. */ { kind: 'owner'; conversationId?: string; slackChannelId?: string; threadTs?: string; chatThreadId?: string; receivedBy?: string; appComment?: { appId: string; commentId: string; }; } /** Work on a project ticket */ | { kind: 'ticket'; projectPath: string; ticketId: string; title?: string; teamId?: string; } /** Fired by a trigger or a cron task */ | { kind: 'trigger'; triggerId?: string; cronTaskId?: string; destination?: string; teamId?: string; topic: string; } /** * Delegated by an agent on its own initiative (no owner conversation named, * crewly#1357): the result goes back to that agent, never to the owner. */ | { kind: 'agent'; sessionName: string; }; /** * Who reports finished team work to the owner, and where * (`metadata.leadReport`, crewly#1371; see `team-lead-report.ts`). */ export interface LeadReport { /** Session of the agent that reports (the team lead; the worker when the team has no lead) */ lead: string; /** Its display name, when known */ leadName?: string; /** Session of the agent that did the work */ worker: string; /** Its display name, when known */ workerName?: string; /** The team */ teamId: string; teamName: string; /** The team's Slack channel */ slackChannelId: string; /** Its name, without the `#` */ channelName: string; } /** * Shape check for a stamped lead report. * * @param value - Anything * @returns True for a {@link LeadReport} */ export declare function isLeadReport(value: unknown): value is LeadReport; /** * The lead report a work item is stamped with (crewly#1371). * * @param wi - Work item * @returns The stamp, or null when its finished work is not reported by a team lead */ export declare function leadReportOf(wi: Pick | null | undefined): LeadReport | null; /** Where the answer goes. */ export type WorkDestination = /** The owner's turn origin — the existing `reply` path */ { kind: 'owner-origin'; origin: TurnOrigin; reason: string; } /** The ticket's Slack thread (created with a `*ID · title*` root when missing) */ | { kind: 'ticket-thread'; projectPath: string; ticketId: string; title?: string; teamId?: string; reason: string; } /** An explicit Slack place (a trigger's destination): channel name/id, optional thread */ | { kind: 'slack'; target: string; threadTs?: string; topic?: string; reason: string; } /** A new top-level post in the agent's (or the work's) team channel */ | { kind: 'new-top-level'; teamId?: string; topic?: string; reason: string; } /** An internal message to the agent that delegated the work (`workItemId`: the work it answers) */ | { kind: 'agent'; sessionName: string; workItemId?: string; reason: string; }; /** * Build the origin stamped on a trigger- or cron-created work item. * * @param input - Trigger/cron ids, its destination, team and a topic * @returns The origin (topic shortened to one line) */ export declare function buildTriggerOrigin(input: { triggerId?: string; cronTaskId?: string; destination?: string; teamId?: string; topic: string; }): WorkItemOrigin; /** * The origin of work an agent delegated on its own initiative: the delegating * agent itself (crewly#1357). Its result is reported to that agent, which * decides what, if anything, the owner hears. * * @param sessionName - The delegating agent's session * @returns An agent origin */ export declare function buildAgentOrigin(sessionName: string): WorkItemOrigin; /** * One-line topic, capped at {@link WORK_ITEM_DESTINATION_CONSTANTS.TOPIC_MAX_CHARS}. * * @param text - Any text * @returns Single line, possibly shortened with an ellipsis */ export declare function shortTopic(text: string | undefined): string; /** * The agent's current work item: the newest (by start) item targeted at it * that is running or accepted. * * @param items - Pool items * @param session - Agent session * @returns The item, or null */ export declare function currentWorkItemOf(items: readonly WorkItem[], session: string): WorkItem | null; /** * The origin of a work item: its stamped `metadata.origin`, else what its * other fields say (a linked project ticket, a trigger, a cron task). * * @param wi - Work item * @returns The origin, or null when nothing tells where it came from */ export declare function originOfWorkItem(wi: WorkItem): WorkItemOrigin | null; /** * The origin an owner turn gives the work started from it. * * @param turn - The owner's turn origin (OrcReplyRouteService) * @param receivedBy - The agent the owner's message reached, when known * @returns An owner origin */ export declare function ownerOriginFromTurn(turn: TurnOrigin, receivedBy?: string): WorkItemOrigin; /** Inputs of {@link inheritedOrigin}. */ export interface InheritOriginInput { /** The item being created */ workItem: WorkItem; /** The item it continues (verify / retry / subtask parent), when it has one */ parent: WorkItem | null; /** Where the creating agent's current work came from (its plan), when an agent creates it */ creatorDestination: WorkDestination | null; /** The creating agent's current work item */ creatorWorkItem: WorkItem | null; /** The creating agent (stamped as `receivedBy` on an owner origin taken from its turn) */ creatorSession?: string; /** * An owner request the creator is handling that is not a chat turn: an * app comment delivered to it, or an owner request a colleague handed it * (see `owner-request-context.ts`). `at` = when it reached the creator. */ creatorOwnerContext?: { origin: WorkItemOrigin; at: number; } | null; /** * True when an agent hands the item to someone else (target is not the * creator). Such a delegation never inherits the creator's owner turn, * owner context or current work: without an owner conversation named * explicitly its origin is the creator itself (crewly#1357). */ delegation?: boolean; /** Clock (epoch ms); defaults to Date.now() */ now?: number; } /** * The origin a new work item inherits (owner request → delegate → verify / * retry / subtask). Incident 2026-10-01: Atlas delegated the owner's * #morning-brief question to Sage; Sage's [DONE] made a verify item for * Atlas with no origin, and the answer's file landed in an unrelated thread. * * Order: an origin already stamped wins (null = keep it); then the parent's; * then the creator's current owner request (the newer of its owner turn and * its owner context — an app comment or a colleague's hand-over — when that * is newer than its current work item); then the creator's current work * item's. * * Incident 2026-10-07: the owner's app comment reached Lyra (no chat turn), * Lyra handed it to Ella by message, and Ella's tickets carried no owner * origin, so nothing linked their completion back to the owner. * * Incident 2026-10-17 (crewly#1357): Ella delegated work of her own to Lyra * and the item took an owner DM from nine days earlier (her running item's * origin); later two delegations made while she answered the owner in a * video thread took that thread. Lyra's internal report and the harness's * "finished" summary both went to the owner. So an agent's DELEGATION * (`delegation: true`) inherits nothing from the creator's turn, context or * current work: its origin is the creator (an agent origin), unless the * caller named the owner conversation (handled before this, by the task * pool) or the item continues a parent. An owner origin read off the * creator's current work item is also dropped once that work is older than * {@link WORK_ITEM_DESTINATION_CONSTANTS.ORIGIN_INHERIT_MAX_AGE_MS}. * * @param input - New item, its parent, the creator's current work * @returns The origin to stamp, or null (nothing to inherit / already stamped) */ export declare function inheritedOrigin(input: InheritOriginInput): WorkItemOrigin | null; /** * Shape check for a stamped origin. * * @param value - Anything * @returns True for a {@link WorkItemOrigin} */ export declare function isWorkItemOrigin(value: unknown): value is WorkItemOrigin; /** * Whether an owner request is too old for new work to inherit its origin * (crewly#1357). An unknown age (`null`) is not too old: nothing says so. * * @param askedAt - When the owner last asked there (epoch ms), or null when unknown * @param now - Clock (epoch ms) * @returns True when older than {@link WORK_ITEM_DESTINATION_CONSTANTS.ORIGIN_INHERIT_MAX_AGE_MS} */ export declare function isOwnerRequestTooOld(askedAt: number | null | undefined, now: number): boolean; /** * Split a trigger destination into a post target and an optional thread. * * @param destination - `#name`, `C…` or `C…:` * @returns Target + thread, or null when unreadable */ export declare function parseDestination(destination: string): { target: string; threadTs?: string; } | null; /** Inputs of {@link planWorkDestination}. */ export interface WorkDestinationInput { /** The agent's current work item (see {@link currentWorkItemOf}) */ workItem: WorkItem | null; /** The agent's last owner turn origin (OrcReplyRouteService) */ ownerOrigin?: TurnOrigin; /** Clock (epoch ms) */ now: number; } /** * Decide where the answer to the agent's current work goes. * * The current work is the newer of the running work item and the last owner * message — the owner message only counts while it is fresh * ({@link WORK_ITEM_DESTINATION_CONSTANTS.OWNER_ORIGIN_FRESH_MS}). * * @param input - Current work item, owner origin, clock * @returns The destination */ export declare function planWorkDestination(input: WorkDestinationInput): WorkDestination; /** * The text of a new top-level post: the topic line in bold, then the body. * * @param topic - Topic (may be empty) * @param text - Body * @returns Post text */ export declare function withTopicLine(topic: string | undefined, text: string): string; /** * The owner origin a delegation names explicitly (`delegate-task --thread * `, specs/2026-10-04-tl-delegation.md §1). * * @param rawKey - Slack thread key (or the whole `[SLACK-THREAD:…]` tag) * @param conversationId - The conversation the thread belongs to, when known * @returns The origin, or null when the key is unreadable */ export declare function ownerOriginFromThreadKey(rawKey: unknown, conversationId?: string | null): WorkItemOrigin | null; /** * The owner origin a delegation names by chat conversation (`delegate-task * --conversation `, crewly#1357). * * @param rawId - Conversation id * @param chatThreadId - Thread root message id in that conversation, when the owner asked in a thread * @returns The origin, or null when the id is unusable */ export declare function ownerOriginFromConversation(rawId: unknown, chatThreadId?: string | null): WorkItemOrigin | null; /** * The line a delegated member gets when its work came from the delegating * agent itself, not from the owner (crewly#1357): the result goes to that * agent, nothing to the owner. * * @param wi - The stored work item * @param delegatorName - Who delegated it (name or session) * @returns Who gets the result + the note, or null when the item has no agent origin */ export declare function delegatorResultNote(wi: WorkItem, delegatorName?: string): { to: string; note: string; } | null; /** * Whether `session` was handed work that came from this owner Slack thread: * a live work item targeted at it whose owner origin is the thread (or one * finished within {@link TL_DELEGATION_CONSTANTS.DONE_THREAD_GRACE_MS}). Its * posts there are answers it owes, never "a second responder". * * @param items - Pool items * @param session - Agent about to post * @param slackChannelId - Thread's Slack channel * @param threadTs - Thread root ts * @param now - Clock (epoch ms) * @returns True when the agent holds work from that thread */ export declare function isDelegatedInThread(items: readonly WorkItem[], session: string, slackChannelId: string, threadTs: string, now: number): boolean; /** * The line a delegated member gets when its work came from an owner Slack * thread: answer there yourself, under your own name. * * @param wi - The stored work item * @param delegatorName - Who delegated it (name or session), for the "does not relay" line * @returns Thread key + note, or null when the item has no owner Slack thread */ export declare function ownerThreadHandover(wi: WorkItem, delegatorName?: string): { key: string; note: string; } | null; //# sourceMappingURL=work-item-destination.d.ts.map