/** * record_directive — the PM's standing instructions ("pilots come first"), * appended to an append-only JSONL ledger in the pm-state repo (decisions.md D4, * pm-state.md). A directive is *intent*: it's logged verbatim; its effects land * later through apply_changes. Merge is concatenation — order derives from `ts` * at read time, so two writers never conflict. `seq` is a display ordinal * computed on read, never stored. This module is pure serialize/parse; the * append (read → concat → write) is the bridge's job. */ export type DirectiveKind = 'reprioritize' | 'reestimate' | 'set-deadline' | 'scope' | 'capacity' | 'note' export type DirectiveStatus = 'proposed' | 'accepted' | 'amended' | 'withdrawn' export interface DirectiveTarget { issue?: number milestone?: number member?: string } /** What the record_directive tool captures. */ export interface DirectiveInput { kind: DirectiveKind /** Verbatim PM words, shown in the ledger. */ quote: string target?: DirectiveTarget /** Structured effect the scheduler applies, e.g. { priority: 1 }. */ params?: Record rationale?: string } /** A ledger entry — an input plus its durable id/ts/status. */ export interface DirectiveEntry extends DirectiveInput { id: string ts: string status: DirectiveStatus } /** A ledger entry as read back, with a computed display ordinal. */ export interface DirectiveRecord extends DirectiveEntry { seq: number } const DIRECTIVE_KINDS: readonly DirectiveKind[] = [ 'reprioritize', 'reestimate', 'set-deadline', 'scope', 'capacity', 'note', ] /** Normalize a raw tool payload into a DirectiveInput (unknown kind → note). */ export function toDirectiveInput(raw: unknown): DirectiveInput { const r = (raw ?? {}) as Record const kind = DIRECTIVE_KINDS.includes(r.kind as DirectiveKind) ? (r.kind as DirectiveKind) : 'note' const target = (r.target ?? undefined) as DirectiveTarget | undefined return { kind, quote: typeof r.quote === 'string' ? r.quote : '', target: target && (target.issue || target.milestone || target.member) ? target : undefined, params: (r.params && typeof r.params === 'object' ? (r.params as Record) : undefined), rationale: typeof r.rationale === 'string' ? r.rationale : undefined, } } /** Build a full entry from an input + externally-supplied id/ts (Date/uuid live in the caller). */ export function makeDirectiveEntry( input: DirectiveInput, id: string, ts: string, status: DirectiveStatus = 'accepted', ): DirectiveEntry { return { ...input, id, ts, status } } /** One JSONL line (no trailing newline — the caller joins). */ export function serializeDirective(entry: DirectiveEntry): string { return JSON.stringify(entry) } /** * Parse a JSONL log into records ordered by `ts` (then id for stability), with a * 1-based `seq` assigned on read. Blank/corrupt lines are skipped, not fatal. */ export function parseDirectiveLog(text: string): DirectiveRecord[] { const entries: DirectiveEntry[] = [] for (const line of text.split('\n')) { const trimmed = line.trim() if (!trimmed) continue try { const e = JSON.parse(trimmed) as DirectiveEntry if (e && typeof e.id === 'string' && typeof e.ts === 'string') entries.push(e) } catch { // skip a corrupt line rather than lose the whole ledger } } entries.sort((a, b) => (a.ts === b.ts ? a.id.localeCompare(b.id) : a.ts.localeCompare(b.ts))) return entries.map((e, i) => ({ ...e, seq: i + 1 })) } /** Append a serialized entry to existing log text (concatenation merge). */ export function appendDirective(existing: string, entry: DirectiveEntry): string { const base = existing.endsWith('\n') || existing === '' ? existing : existing + '\n' return `${base}${serializeDirective(entry)}\n` }