/** * Append-only approval audit logger (Sprint 13). * * Each call to recordApproval() appends ONE JSON line to * .bober/audits/.jsonl. Lines never span. Concurrent appends from * multiple async checkpoints serialize via an in-process Promise chain * (per-runId mutex — unrelated runs proceed in parallel). * * File is created with mode 0600 on first append via fs.open with * O_WRONLY|O_APPEND|O_CREAT flags. Subsequent appends preserve the mode * (kernel does not re-chmod on O_APPEND). appendFile is NOT used because * it does not reliably honor the mode argument across all Node versions. * * POSIX O_APPEND atomicity: single-line records are well under PIPE_BUF * (4096 bytes), so cross-process appends are also safe in practice. * Multi-process safety is not formally guaranteed beyond PIPE_BUF limits. * * approverId resolution: chooses a strategy per mechanism name. * 1. PR mechanism: GitHub user from comment/merge actor (passed in). * 2. CLI mechanism: process.env["USER"] || process.env["USERNAME"]. * 3. disk mechanism: `git config user.name` then env USER. * 4. noop mechanism: 'autopilot'. * 5. fallback: 'unknown'. * * Audit write failures NEVER break the pipeline — the finally block in * runWithAudit swallows recordApproval errors via .catch(() => {}). * * Mechanism fallback chains (cli→noop, pr→disk) are transparent to this * module. The audit records the *requested* mechanism name, not the actual * fallback that fired inside the mechanism. * * Sprint 13 — colocated in src/orchestrator/checkpoints/ per Sprints 7-12 precedent. */ import type { CheckpointOutcome } from "./types.js"; export type ApprovalOutcome = "approved" | "rejected" | "edited" | "aborted"; export type MechanismName = "cli" | "disk" | "pr" | "noop"; export interface EditDeltaSummary { /** Number of lines in the after-text. */ lineCount: number; /** First 200 characters of the after-text. */ firstChars: string; } export interface ApprovalRecord { /** ISO-8601 timestamp set at write time. */ timestamp: string; runId: string; /** Widened to string: CheckpointId values are valid here but not enforced at runtime. */ checkpointId: string; mechanism: MechanismName; outcome: ApprovalOutcome; approverId: string; /** 1-based iteration count (per checkpoint invocation, not global). */ iteration: number; /** First 500 chars of feedback text, if any. */ feedbackText?: string; /** Null when outcome is not 'edited'. */ editDeltaSummary?: EditDeltaSummary | null; durationMs: number; } export declare function getAuditPath(projectRoot: string, runId: string): string; /** * Append one ApprovalRecord to .bober/audits/.jsonl. * * Writes are serialized per-runId via a Promise chain so concurrent async * callers for the same run never interleave partial lines. * * Propagates errors to the caller but does NOT break the chain — subsequent * appends for the same runId will still proceed. */ export declare function recordApproval(projectRoot: string, runId: string, record: ApprovalRecord): Promise; /** * Resolve the identity of the approver from the mechanism name and optional hint. * * Resolution chain (per generatorNotes): * 1. pr → `hint` (GitHub actor passed by caller) or 'github:unknown' * 2. cli → process.env["USER"] || process.env["USERNAME"] || 'unknown' * 3. disk → `git config user.name` (with 5s timeout, reject:false) → env USER → 'unknown' * 4. noop → 'autopilot' * 5. fallback → 'unknown' */ export declare function resolveApproverId(mechanism: MechanismName, hint?: string): Promise; /** * Extract a compact summary from an editDelta value. * Returns null if the value cannot be coerced to a meaningful string. * * Rules: * - string editDelta → after-text is the string itself * - { after: string } → use `after` * - { before, after } or any other object → JSON.stringify * - null/undefined → null */ export declare function summarizeEditDelta(editDelta: unknown): EditDeltaSummary | null; /** * Truncate feedback text to 500 characters to minimize PII surface in the audit log. * Returns undefined if the input is undefined. */ export declare function truncateFeedback(s: string | undefined): string | undefined; /** * Wrap a mechanism.request() call with audit accounting in a try/finally. * * This is the canonical seam (B) — every caller (pipeline.ts × 9 sites, * feedback-router's runCheckpointWithFeedback) calls runWithAudit instead * of mechanism.request() directly. The wrapper owns the try/finally so the * audit entry is recorded even when the mechanism throws. * * Outcome mapping: * { approved: true } → 'approved' * { approved: false } → 'rejected' (feedbackText from outcome.feedback) * { edit: true } → 'edited' (editDeltaSummary from outcome.editDelta) * thrown error → 'aborted' (feedbackText from err.message) * * Audit write failures NEVER break the pipeline — they are swallowed via * .catch(() => {}) after a logger.warn. The mechanism's outcome is always * returned to the caller. * * Re-throws the original error after writing the audit entry so callers see * the mechanism failure. */ export declare function runWithAudit(opts: { projectRoot: string; runId: string; checkpointId: string; mechanism: MechanismName; iteration: number; approverHint?: string; fn: () => Promise; }): Promise; //# sourceMappingURL=audit.d.ts.map