Intelligence

Artifacts

Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.

Repository
README.md
CONSTITUTION_COMPLIANCE_AUDIT_V1.mdREADME.md
repositories/aaf-holdings/hq01/lib/missions/reports.ts
9.2 KB
import fs from "node:fs";
import path from "node:path";
import { getSession } from "@/lib/sessions/manager";
import { readInstance } from "@/lib/workers/instances";
import { missionDir, readMission } from "./store";
import { getObjective } from "./objectives";
import { readWorkOrder } from "./work-orders";
import type {
  Assignment,
  Report,
  ReportView,
} from "./types";

/**
 * Reports (PASS M4) — immutable, structured, fully-traceable evidence stored
 * under each assignment. Every execution produces a new report; reports are
 * never overwritten. This module records evidence only — no promotion, no
 * reflection, no intelligence.
 */

const RID_RE = /^REPORT-\d{6}$/;

function nowIso(): string {
  return new Date().toISOString();
}
function assignmentsDir(mid: string): string {
  return path.join(missionDir(mid), "assignments");
}
function reportsDir(mid: string, aid: string): string {
  return path.join(assignmentsDir(mid), aid, "reports");
}
function reportMdPath(mid: string, aid: string, rid: string): string {
  return path.join(reportsDir(mid, aid), `${rid}.md`);
}
function reportJsonPath(mid: string, aid: string, rid: string): string {
  return path.join(reportsDir(mid, aid), `${rid}.json`);
}

/** Read an assignment.json directly (avoids importing the assignments module). */
function readAssignmentRaw(mid: string, aid: string): Assignment | null {
  try {
    return JSON.parse(
      fs.readFileSync(path.join(assignmentsDir(mid), aid, "assignment.json"), "utf8"),
    ) as Assignment;
  } catch {
    return null;
  }
}

function writeOnce(file: string, content: string): void {
  fs.mkdirSync(path.dirname(file), { recursive: true });
  // "wx": never overwrite an existing report.
  const fd = fs.openSync(file, "wx");
  try {
    fs.writeFileSync(fd, content);
  } finally {
    fs.closeSync(fd);
  }
}

/**
 * Parse the agent's report.md into structured fields by `## ` headings. List
 * sections collect `- ` bullets; prose sections keep their text. Unknown or
 * missing sections are ignored / left empty.
 */
interface ParsedReport {
  summary: string;
  files_changed: string[];
  files_created: string[];
  commands_executed: string[];
  artifacts_produced: string[];
  recommendations: string[];
  doctrine_candidates: string[];
  next_pass: string;
}

function normalizeHeading(h: string): string {
  return h.toLowerCase().replace(/[^a-z ]/g, "").trim();
}

const LIST_FIELDS: Record<string, keyof ParsedReport> = {
  "files changed": "files_changed",
  "files created": "files_created",
  "commands executed": "commands_executed",
  "artifacts produced": "artifacts_produced",
  artifacts: "artifacts_produced",
  recommendations: "recommendations",
  "doctrine candidates": "doctrine_candidates",
};
const PROSE_FIELDS: Record<string, keyof ParsedReport> = {
  summary: "summary",
  "next pass recommendation": "next_pass",
  "recommended next pass": "next_pass",
  "next pass": "next_pass",
};

export function parseReportMarkdown(md: string): ParsedReport {
  const out: ParsedReport = {
    summary: "",
    files_changed: [],
    files_created: [],
    commands_executed: [],
    artifacts_produced: [],
    recommendations: [],
    doctrine_candidates: [],
    next_pass: "",
  };
  const sections: Record<string, string[]> = {};
  let current: string | null = null;
  for (const raw of md.split(/\r?\n/)) {
    const h = raw.match(/^#{1,6}\s+(.*)$/);
    if (h) {
      current = normalizeHeading(h[1]);
      if (!sections[current]) sections[current] = [];
      continue;
    }
    if (current) sections[current].push(raw);
  }
  for (const [heading, lines] of Object.entries(sections)) {
    const bullets = lines
      .map((l) => l.match(/^\s*[-*]\s+(.*)$/))
      .filter((m): m is RegExpMatchArray => Boolean(m))
      .map((m) => m[1].trim())
      .filter(Boolean);
    const prose = lines.join("\n").trim();
    const listField = LIST_FIELDS[heading];
    const proseField = PROSE_FIELDS[heading];
    if (listField) {
      // Preserve doctrine candidates exactly: keep all non-empty lines if no bullets.
      (out[listField] as string[]) =
        bullets.length > 0
          ? bullets
          : lines.map((l) => l.trim()).filter(Boolean);
    } else if (proseField) {
      (out[proseField] as string) = prose;
    }
  }
  return out;
}

function nextReportId(mid: string, aid: string): string {
  let max = 0;
  let names: string[] = [];
  try {
    names = fs.readdirSync(reportsDir(mid, aid));
  } catch {
    /* none */
  }
  for (const n of names) {
    const m = n.match(/^REPORT-(\d{6})\.json$/);
    if (m) {
      const num = Number(m[1]);
      if (num > max) max = num;
    }
  }
  return `REPORT-${String(max + 1).padStart(6, "0")}`;
}

export function listReportsForAssignment(mid: string, aid: string): Report[] {
  let names: string[];
  try {
    names = fs.readdirSync(reportsDir(mid, aid));
  } catch {
    return [];
  }
  const out: Report[] = [];
  for (const n of names) {
    if (!n.endsWith(".json")) continue;
    try {
      out.push(JSON.parse(fs.readFileSync(reportJsonPath(mid, aid, n.replace(/\.json$/, "")), "utf8")) as Report);
    } catch {
      /* skip */
    }
  }
  return out.sort((a, b) => b.created_at.localeCompare(a.created_at));
}

function listAssignmentIds(mid: string): string[] {
  try {
    return fs
      .readdirSync(assignmentsDir(mid), { withFileTypes: true })
      .filter((e) => e.isDirectory() && /^ASSIGNMENT-\d{6}$/.test(e.name))
      .map((e) => e.name);
  } catch {
    return [];
  }
}

export function listReportsForMission(mid: string): Report[] {
  return listAssignmentIds(mid)
    .flatMap((aid) => listReportsForAssignment(mid, aid))
    .sort((a, b) => b.created_at.localeCompare(a.created_at));
}

export function listReportsForObjective(mid: string, oid: string): Report[] {
  return listReportsForMission(mid).filter((r) => r.objective_id === oid);
}

export function listReportsForWorkOrder(mid: string, wid: string): Report[] {
  return listReportsForMission(mid).filter((r) => r.work_order_id === wid);
}

export function getReport(mid: string, aid: string, rid: string): Report | null {
  if (!RID_RE.test(rid)) return null;
  try {
    return JSON.parse(fs.readFileSync(reportJsonPath(mid, aid, rid), "utf8")) as Report;
  } catch {
    return null;
  }
}

export function getReportView(mid: string, aid: string, rid: string): ReportView | null {
  const report = getReport(mid, aid, rid);
  if (!report) return null;
  let markdown = "";
  try {
    markdown = fs.readFileSync(reportMdPath(mid, aid, rid), "utf8");
  } catch {
    markdown = "";
  }
  const assignment = readAssignmentRaw(mid, aid);
  return {
    report,
    markdown,
    mission: readMission(mid),
    objective: getObjective(mid, report.objective_id),
    work_order: readWorkOrder(mid, report.work_order_id),
    assignment,
  };
}

/**
 * Generate the structured, immutable report for a completed assignment session,
 * if one has not already been recorded for that session. Returns the new report
 * id, or null if nothing was generated.
 */
export function generateReportForAssignment(mid: string, aid: string): string | null {
  const assignment = readAssignmentRaw(mid, aid);
  if (!assignment || !assignment.session) return null;
  const sid = assignment.session.session_id;

  // Idempotent: one report per execution (session).
  if (listReportsForAssignment(mid, aid).some((r) => r.session_id === sid)) {
    return null;
  }
  const session = getSession(sid);
  if (!session) return null;

  let markdown = "";
  const candidates = [
    path.join(session.runtime_write_root || session.working_directory, "report.md"),
    path.join(session.runtime_write_root || session.working_directory, "outputs", "report.md"),
  ];
  for (const p of candidates) {
    try {
      markdown = fs.readFileSync(p, "utf8");
      if (markdown) break;
    } catch {
      /* try next */
    }
  }

  const parsed = parseReportMarkdown(markdown);
  const rid = nextReportId(mid, aid);

  // Worker provenance (PASS M7).
  const instance = assignment.worker_instance_id
    ? readInstance(assignment.worker_instance_id)
    : null;
  let executionDuration: string | null = null;
  if (session.started_at && session.stopped_at) {
    const secs = Math.max(
      0,
      Math.round((Date.parse(session.stopped_at) - Date.parse(session.started_at)) / 1000),
    );
    executionDuration = secs >= 60 ? `${Math.floor(secs / 60)}m ${secs % 60}s` : `${secs}s`;
  }

  const report: Report = {
    report_id: rid,
    mission_id: mid,
    objective_id: assignment.objective_id,
    work_order_id: assignment.work_order_id,
    assignment_id: aid,
    session_id: sid,
    executive: assignment.executive,
    repository: assignment.repository,
    workspace: assignment.workspace,
    started_at: session.started_at,
    completed_at: session.stopped_at,
    status: session.status,
    ...parsed,
    created_at: nowIso(),
    worker_template_id: instance?.template_id ?? assignment.worker_template ?? null,
    worker_instance_id: assignment.worker_instance_id ?? null,
    worker_version: instance?.version ?? null,
    model_used: instance?.model ?? null,
    execution_duration: executionDuration,
  };

  // Immutable: written once, never overwritten.
  writeOnce(reportMdPath(mid, aid, rid), markdown || "(no report.md was produced)\n");
  writeOnce(reportJsonPath(mid, aid, rid), JSON.stringify(report, null, 2) + "\n");
  return rid;
}

root · /srv/aaf