feat(gsd-uok): unify audit envelopes across logger metrics and activity

This commit is contained in:
Jeremy McSpadden 2026-04-14 20:41:38 -05:00
parent d6c93ef07f
commit 558ac1067b
7 changed files with 213 additions and 0 deletions

View file

@ -16,6 +16,8 @@ import { GSDError, GSD_IO_ERROR } from "./errors.js";
const SEQ_PREFIX_RE = /^(\d+)-/;
import type { ExtensionContext } from "@gsd/pi-coding-agent";
import { gsdRoot } from "./paths.js";
import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js";
interface ActivityLogState {
nextSeq: number;
@ -132,6 +134,25 @@ export function saveActivityLog(
}
state.nextSeq += 1;
state.lastSnapshotKeyByUnit.set(unitKey, key);
if (isUnifiedAuditEnabled()) {
emitUokAuditEvent(
basePath,
buildAuditEnvelope({
traceId: `activity:${unitType}:${unitId}`,
turnId: unitId,
category: "execution",
type: "activity-log-saved",
payload: {
unitType,
unitId,
filePath,
entryCount: entries.length,
},
}),
);
}
return filePath;
} catch (e) {
// Don't let logging failures break auto-mode

View file

@ -15,6 +15,8 @@
import { appendFileSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { gsdRoot } from "./paths.js";
import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js";
// ─── Types ────────────────────────────────────────────────────────────────────
@ -90,6 +92,34 @@ export function emitJournalEvent(basePath: string, entry: JournalEntry): void {
} catch {
// Silent failure — journal must never break auto-mode
}
if (!isUnifiedAuditEnabled()) return;
try {
const causedBy = entry.causedBy
? `${entry.causedBy.flowId}:${entry.causedBy.seq}`
: undefined;
const turnId =
typeof entry.data?.turnId === "string"
? entry.data.turnId
: undefined;
emitUokAuditEvent(
basePath,
buildAuditEnvelope({
traceId: entry.flowId,
turnId,
causedBy,
category: "orchestration",
type: `journal-${entry.eventType}`,
payload: {
seq: entry.seq,
rule: entry.rule,
data: entry.data ?? {},
},
}),
);
} catch {
// Best-effort: audit projection must never block journal writes.
}
}
// ─── Query ────────────────────────────────────────────────────────────────────

View file

@ -19,6 +19,8 @@ import { gsdRoot } from "./paths.js";
import { getAndClearSkills } from "./skill-telemetry.js";
import { loadJsonFile, loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
import { parseUnitId } from "./unit-id.js";
import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js";
// Re-export from shared — import directly from format-utils to avoid pulling
// in the full barrel (mod.js → ui.js → @gsd/pi-tui) which breaks when loaded
@ -143,6 +145,9 @@ export function snapshotUnitMetrics(
promptCharCount?: number;
baselineCharCount?: number;
autoSessionKey?: string;
traceId?: string;
turnId?: string;
causedBy?: string;
},
): UnitMetrics | null {
if (!ledger) return null;
@ -235,6 +240,27 @@ export function snapshotUnitMetrics(
}
saveLedger(basePath, ledger);
if (isUnifiedAuditEnabled()) {
emitUokAuditEvent(
basePath,
buildAuditEnvelope({
traceId: opts?.traceId ?? `metrics:${unitType}:${unitId}`,
turnId: opts?.turnId,
causedBy: opts?.causedBy,
category: "metrics",
type: "unit-metrics-snapshot",
payload: {
unitType,
unitId,
model,
tokens: unit.tokens,
cost: unit.cost,
toolCalls: unit.toolCalls,
},
}),
);
}
return unit;
}

View file

@ -0,0 +1,101 @@
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { emitJournalEvent } from "../journal.ts";
import { saveActivityLog } from "../activity-log.ts";
import { initMetrics, resetMetrics, snapshotUnitMetrics } from "../metrics.ts";
import { setLogBasePath, logWarning } from "../workflow-logger.ts";
import { setUnifiedAuditEnabled } from "../uok/audit-toggle.ts";
function readAuditEvents(basePath: string): Array<Record<string, unknown>> {
const file = join(basePath, ".gsd", "audit", "events.jsonl");
if (!existsSync(file)) return [];
const raw = readFileSync(file, "utf-8");
return raw
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line) as Record<string, unknown>);
}
function makeMockContext(entries: unknown[]): any {
return {
sessionManager: {
getEntries: () => entries,
},
};
}
test("unified audit plane bridges journal/activity/metrics/workflow logger into audit envelope log", () => {
const basePath = mkdtempSync(join(tmpdir(), "gsd-uok-audit-"));
setUnifiedAuditEnabled(true);
try {
emitJournalEvent(basePath, {
ts: new Date().toISOString(),
flowId: "trace-123",
seq: 1,
eventType: "iteration-start",
data: { turnId: "turn-123", unitId: "M001/S01/T01" },
});
const activityCtx = makeMockContext([
{ type: "message", message: { role: "assistant", content: [{ type: "text", text: "hello" }] } },
]);
const activityPath = saveActivityLog(activityCtx, basePath, "execute-task", "M001/S01/T01");
assert.ok(activityPath);
initMetrics(basePath);
const metricsCtx = makeMockContext([
{
type: "message",
message: {
role: "assistant",
usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, totalTokens: 15, cost: 0.01 },
content: [],
},
},
]);
const unit = snapshotUnitMetrics(
metricsCtx,
"execute-task",
"M001/S01/T01",
Date.now() - 1000,
"openai/gpt-5.4",
{ traceId: "trace-123", turnId: "turn-123" },
);
assert.ok(unit);
resetMetrics();
setLogBasePath(basePath);
logWarning("engine", "audit bridge check", { id: "turn-123" });
const events = readAuditEvents(basePath);
const types = new Set(events.map((event) => String(event.type ?? "")));
assert.ok(types.has("journal-iteration-start"));
assert.ok(types.has("activity-log-saved"));
assert.ok(types.has("unit-metrics-snapshot"));
assert.ok(types.has("workflow-log-warn"));
} finally {
setUnifiedAuditEnabled(false);
resetMetrics();
rmSync(basePath, { recursive: true, force: true });
}
});
test("unified audit bridge is disabled when toggle is off", () => {
const basePath = mkdtempSync(join(tmpdir(), "gsd-uok-audit-off-"));
setUnifiedAuditEnabled(false);
try {
emitJournalEvent(basePath, {
ts: new Date().toISOString(),
flowId: "trace-off",
seq: 1,
eventType: "iteration-start",
});
const events = readAuditEvents(basePath);
assert.equal(events.length, 0);
} finally {
rmSync(basePath, { recursive: true, force: true });
}
});

View file

@ -0,0 +1,9 @@
const AUDIT_ENV_KEY = "GSD_UOK_AUDIT_UNIFIED";
export function setUnifiedAuditEnabled(enabled: boolean): void {
process.env[AUDIT_ENV_KEY] = enabled ? "1" : "0";
}
export function isUnifiedAuditEnabled(): boolean {
return process.env[AUDIT_ENV_KEY] === "1";
}

View file

@ -6,6 +6,7 @@ import type { AutoSession } from "../auto/session.js";
import type { LoopDeps } from "../auto/loop-deps.js";
import { gsdRoot } from "../paths.js";
import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
import { setUnifiedAuditEnabled } from "./audit-toggle.js";
import { resolveUokFlags } from "./flags.js";
import { createTurnObserver } from "./loop-adapter.js";
@ -39,6 +40,7 @@ export async function runAutoLoopWithUok(args: RunAutoLoopWithUokArgs): Promise<
const { ctx, pi, s, deps, runLegacyLoop } = args;
const prefs = deps.loadEffectiveGSDPreferences()?.preferences;
const flags = resolveUokFlags(prefs);
setUnifiedAuditEnabled(flags.auditUnified);
writeParityEvent(s.basePath, {
ts: new Date().toISOString(),

View file

@ -20,6 +20,8 @@ import { appendFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import { appendNotification } from "./notification-store.js";
import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js";
// ─── Types ──────────────────────────────────────────────────────────────
@ -275,6 +277,28 @@ function _push(
_buffer.shift();
}
if (_auditBasePath && isUnifiedAuditEnabled()) {
try {
emitUokAuditEvent(
_auditBasePath,
buildAuditEnvelope({
traceId: `workflow-log:${component}`,
turnId: context?.id,
causedBy: context?.fn ?? context?.tool,
category: "orchestration",
type: severity === "error" ? "workflow-log-error" : "workflow-log-warn",
payload: {
component,
message,
context: context ?? {},
},
}),
);
} catch {
// Best-effort: unified audit projection must never block workflow logger.
}
}
// Persist errors to .gsd/audit-log.jsonl so they survive context resets.
// Only error-severity entries are persisted — warnings are ephemeral (stderr + buffer)
// to avoid log amplification from expected-control-flow catch paths.