/** * Work records (#1349) — the pure half. * * A work record is one structured `(record)` entry in a vault's activity log * describing a finished (or long-blocked) task: who asked for it, who did it, * what came out, what went wrong on the way and how it was resolved, who was * pulled in, and how it ended. This module knows the shape, assembles a * record from already-loaded inputs (no I/O, so fixtures can drive it), * renders it as a log body, and parses a body back for the digest and the * startup briefing. * * Owner-facing lines never carry ticket ids or WorkItem ids; those live on * the `meta:` line, which the digest reads but never renders. * * Body layout (one field per line, parse-friendly): * * (record) **** — <outcome phrase> * asked by: <who> — "<quote>" * done by: <name> * did: <summary> * outputs: <a>; <b> * errors: <what> (fixed); <what> (unresolved) * helpers: delegated to Leo: "<title>" → done; asked Ella → answered; reviewed by Owen * outcome: <phrase> * meta: wi=<id> phase=final status=verified asked_by=owen:lead done_by=nova … * * @module services/wiki/work-record */ import type { WorkItem } from '../../types/v2/work-item.types.js'; import type { ProjectTicket } from '../../types/project-ticket.types.js'; import type { Request } from '../../types/v2/request.types.js'; import type { TraceEvent } from '../trace/trace.types.js'; import type { WikiLogEntryRecord } from './wiki-log-entries.js'; import type { TranscriptWindowFacts } from './work-record-transcript.js'; /** Who a party to the task is. */ export type WorkRecordPartyKind = 'owner' | 'orchestrator' | 'lead' | 'agent' | 'schedule' | 'system'; /** How the task ended. */ export type WorkRecordOutcome = 'owner_verified' | 'lead_verified' | 'silence_accepted' | 'done' | 'rejected' | 'cancelled' | 'failed' | 'blocked' | 'waiting_owner'; /** `final` closes the task; `blocked` is an interim record for a parked task. */ export type WorkRecordPhase = 'final' | 'blocked'; /** One party (asker, doer, helper). */ export interface WorkRecordParty { kind: WorkRecordPartyKind; /** Display name (`Owen`, `the owner`, `schedule "Daily ops"`). */ name: string; /** Agent session when the party is an agent. */ session?: string; } /** Someone pulled in while the task ran. */ export interface WorkRecordHelper { how: 'delegated' | 'asked' | 'reviewed'; name: string; session?: string; /** Delegated sub-task title. */ what?: string; /** What came back (`done`, `rejected`, `answered`, `no answer`). */ back?: string; } /** An error met on the way. */ export interface WorkRecordError { what: string; resolved: 'fixed' | 'unresolved' | 'unknown'; } /** The assembled record. */ export interface WorkRecord { workItemId: string; phase: WorkRecordPhase; title: string; askedBy: WorkRecordParty; /** Where the ask came from, when known (`Slack thread`, `chat`, `app comment`). */ askedWhere?: string; /** The person the asker acted for (an owner-origin delegation by a lead). */ forOwner: boolean; askQuote: string; doneBy: WorkRecordParty; did: string; outputs: string[]; errors: WorkRecordError[]; helpers: WorkRecordHelper[]; outcome: WorkRecordOutcome; /** Who decided the outcome (reviewer, owner), display name. */ outcomeBy?: string; /** Cancel / block / rejection reason. */ outcomeNote?: string; endedAt: string; durationMs?: number; /** Machine-readable fields for the digest (ticket ids live here only). */ meta: Record<string, string>; } /** Everything the assembler needs, already loaded. */ export interface WorkRecordInputs { workItem: WorkItem; phase: WorkRecordPhase; /** Linked project ticket (`metadata.projectTicket`), when found. */ ticket?: ProjectTicket | null; /** Linked Request (`workItem.requestId`), when found. */ request?: Request | null; /** Child items: delegations and the verify item. */ children?: readonly WorkItem[]; /** The run trace's events, when a trace exists. */ trace?: readonly TraceEvent[]; /** Facts from the doer's transcript for the task window. */ transcript?: TranscriptWindowFacts | null; /** Display name of an agent session. */ nameOf: (session: string) => string; /** Party kind of an agent session (`lead`, `orchestrator`, `agent`). */ kindOf: (session: string) => WorkRecordPartyKind; /** Ticket prefixes to strip from owner-facing text (`CE`, `CREW`). */ ticketPrefixes?: readonly string[]; /** Clock. */ now?: number; } /** * `work-record:<workItemId>#<phase>` — the entry's `ref:` and its dedupe key. * * @param workItemId - WorkItem id * @param phase - Record phase * @returns Stable ref */ export declare function workRecordRef(workItemId: string, phase: WorkRecordPhase): string; /** * Parse a record ref back into its parts. * * @param ref - `ref:` value * @returns Parts, or null when it is not a record ref */ export declare function parseWorkRecordRef(ref: string): { workItemId: string; phase: WorkRecordPhase; } | null; /** * Whether a log entry is a work record. * * @param entry - Parsed log entry * @returns True for `work_record` entries (by source type or ref) */ export declare function isWorkRecordEntry(entry: Pick<WikiLogEntryRecord, 'sourceType' | 'ref'>): boolean; /** * Whether a WorkItem is the verify item of another (`<id>:verify:<id>` or * `metadata.verifyOf`). Verify items are folded into the parent's record. * * @param wi - WorkItem * @returns True for a verify item */ export declare function isVerifyWorkItem(wi: Pick<WorkItem, 'id' | 'metadata' | 'type'>): boolean; /** * Whether a WorkItem gets a record at all: real work, not a verify item or a * bookkeeping type. * * @param wi - WorkItem * @returns True when the item is recordable */ export declare function isRecordableWorkItem(wi: Pick<WorkItem, 'id' | 'metadata' | 'type'>): boolean; /** * The moment an item stopped: `completedAt`, else `statusChangedAt`, else * `createdAt`. * * @param wi - WorkItem * @returns ISO time */ export declare function workItemEndedAt(wi: Pick<WorkItem, 'completedAt' | 'statusChangedAt' | 'createdAt'>): string; /** * Owner-facing text: ticket ids, WorkItem ids and uuids removed, whitespace * collapsed, clipped. * * @param text - Raw text * @param prefixes - Ticket prefixes to strip * @param max - Character cap * @returns Clean one-line text */ export declare function cleanOwnerText(text: string | undefined | null, prefixes?: readonly string[], max?: number): string; /** * Clip to `max` characters with an ellipsis. * * @param text - Text * @param max - Cap * @returns Clipped text */ export declare function clip(text: string, max: number): string; /** * Assemble a record from loaded inputs. Pure. * * @param inputs - WorkItem, links, trace, transcript facts and name resolvers * @returns The record * * @example * ```typescript * const record = assembleWorkRecord({ workItem, phase: 'final', ticket, children, trace, nameOf, kindOf }); * const body = renderWorkRecordBody(record); * ``` */ export declare function assembleWorkRecord(inputs: WorkRecordInputs): WorkRecord; /** * Render a record as the log body. Always fits `MAX_CHARS`; the `meta:` line * is never cut. * * @param record - Assembled record * @returns Markdown body */ export declare function renderWorkRecordBody(record: WorkRecord): string; /** A record as read back from a log body. */ export interface ParsedWorkRecord { title: string; askedBy: string; doneBy: string; did: string; outputs: string[]; errors: string[]; helpers: Array<{ how: 'delegated' | 'asked' | 'reviewed'; name: string; rest: string; }>; outcome: string; meta: Record<string, string>; } /** * Parse a record body written by {@link renderWorkRecordBody}. * * @param body - Log entry body * @returns The fields, or null when the body is not a record */ export declare function parseWorkRecordBody(body: string): ParsedWorkRecord | null; /** * One-line form of a record for the startup briefing and the digest. * * @param parsed - Parsed record * @param max - Character cap * @returns `**Title** — asked by X, done by Y → outcome` */ export declare function shortWorkRecordLine(parsed: ParsedWorkRecord, max?: number): string; /** * The outcome phrase used in the headline and the `outcome:` line. * * @param record - Record * @returns Owner-facing phrase */ export declare function outcomePhrase(record: Pick<WorkRecord, 'outcome' | 'outcomeBy'>): string; //# sourceMappingURL=work-record.d.ts.map