Intelligence
Artifacts
Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.
Repository
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