diff --git a/apps/api/src/server.integration.test.ts b/apps/api/src/server.integration.test.ts
index fd6ed94..ed867e7 100644
--- a/apps/api/src/server.integration.test.ts
+++ b/apps/api/src/server.integration.test.ts
@@ -3842,4 +3842,511 @@ describe("@runroot/api integration", () => {
await peerApp.close();
}
});
+
+ it("serves acknowledge, list-acknowledged, inspect-acknowledgment, clear-acknowledgment, and apply paths through the network API", async () => {
+ const workspaceRoot = await mkdtemp(
+ join(tmpdir(), "runroot-api-checklist-acknowledgment-"),
+ );
+ const sqlitePath = join(workspaceRoot, "runroot.sqlite");
+ const inlineOperator = createRunrootOperatorService({
+ executionMode: "inline",
+ persistenceDriver: "sqlite",
+ sqlitePath,
+ });
+ const queuedOperator = createRunrootOperatorService({
+ executionMode: "queued",
+ persistenceDriver: "sqlite",
+ sqlitePath,
+ });
+ const workerService = (
+ await import("@runroot/sdk")
+ ).createRunrootWorkerService({
+ persistenceDriver: "sqlite",
+ sqlitePath,
+ workerId: "worker_acknowledgment_api",
+ });
+ const ownerApp = buildServer({
+ operator: createRunrootOperatorService({
+ catalogEntryIdGenerator: () => "catalog_entry_acknowledgment_api",
+ operatorId: "ops_oncall",
+ operatorScopeId: "ops",
+ persistenceDriver: "sqlite",
+ savedViewIdGenerator: () => "saved_view_acknowledgment_api",
+ sqlitePath,
+ }),
+ });
+ const peerApp = buildServer({
+ operator: createRunrootOperatorService({
+ operatorId: "ops_backup",
+ operatorScopeId: "ops",
+ persistenceDriver: "sqlite",
+ sqlitePath,
+ }),
+ });
+
+ try {
+ const inlineRun = await inlineOperator.startRun({
+ input: {
+ approvalRequired: false,
+ commandAlias: "print-ready",
+ runbookId: "node-health-check",
+ },
+ templateId: "shell-runbook-flow",
+ });
+ const queuedRun = await queuedOperator.startRun({
+ input: {
+ approvalRequired: false,
+ commandAlias: "print-ready",
+ runbookId: "node-health-check",
+ },
+ templateId: "shell-runbook-flow",
+ });
+
+ await workerService.processNextJob();
+
+ const ownerAddress = await ownerApp.listen({
+ host: "127.0.0.1",
+ port: 0,
+ });
+ const peerAddress = await peerApp.listen({
+ host: "127.0.0.1",
+ port: 0,
+ });
+
+ const saveResponse = await fetch(`${ownerAddress}/audit/saved-views`, {
+ body: JSON.stringify({
+ description: "Queued worker acknowledgment preset",
+ name: "Queued worker acknowledgment preset",
+ navigation: {
+ drilldown: {
+ workerId: "worker_acknowledgment_api",
+ },
+ summary: {
+ executionMode: "queued",
+ },
+ },
+ refs: {
+ auditViewRunId: queuedRun.id,
+ drilldownRunId: queuedRun.id,
+ },
+ }),
+ headers: {
+ "content-type": "application/json",
+ },
+ method: "POST",
+ });
+ const savedViewPayload = (await saveResponse.json()) as {
+ savedView: {
+ id: string;
+ };
+ };
+ const publishResponse = await fetch(`${ownerAddress}/audit/catalog`, {
+ body: JSON.stringify({
+ savedViewId: savedViewPayload.savedView.id,
+ }),
+ headers: {
+ "content-type": "application/json",
+ },
+ method: "POST",
+ });
+ const publishedPayload = (await publishResponse.json()) as {
+ catalogEntry: {
+ entry: {
+ id: string;
+ };
+ };
+ };
+ const shareResponse = await fetch(
+ `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/share`,
+ {
+ method: "POST",
+ },
+ );
+ const reviewResponse = await fetch(
+ `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/review`,
+ {
+ body: JSON.stringify({
+ note: `Acknowledgment ready after inline ${inlineRun.id} and queued ${queuedRun.id}`,
+ state: "recommended",
+ }),
+ headers: {
+ "content-type": "application/json",
+ },
+ method: "POST",
+ },
+ );
+ const assignmentResponse = await fetch(
+ `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/assignment`,
+ {
+ body: JSON.stringify({
+ assigneeId: "ops_backup",
+ handoffNote: `Queued worker ${queuedRun.id} handed to backup`,
+ }),
+ headers: {
+ "content-type": "application/json",
+ },
+ method: "POST",
+ },
+ );
+ const checklistResponse = await fetch(
+ `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/checklist`,
+ {
+ body: JSON.stringify({
+ items: ["Validate queued follow-up", "Close backup handoff"],
+ state: "pending",
+ }),
+ headers: {
+ "content-type": "application/json",
+ },
+ method: "POST",
+ },
+ );
+ const progressResponse = await fetch(
+ `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/progress`,
+ {
+ body: JSON.stringify({
+ completionNote: "Queued follow-up is almost complete",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "completed",
+ },
+ {
+ item: "Close backup handoff",
+ state: "pending",
+ },
+ ],
+ }),
+ headers: {
+ "content-type": "application/json",
+ },
+ method: "POST",
+ },
+ );
+ const blockerResponse = await fetch(
+ `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/blocker`,
+ {
+ body: JSON.stringify({
+ blockerNote: "Waiting for the overnight handoff",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "cleared",
+ },
+ {
+ item: "Close backup handoff",
+ state: "blocked",
+ },
+ ],
+ }),
+ headers: {
+ "content-type": "application/json",
+ },
+ method: "POST",
+ },
+ );
+ const resolutionResponse = await fetch(
+ `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/resolution`,
+ {
+ body: JSON.stringify({
+ resolutionNote: "Backup confirmed the follow-up closure",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "resolved",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unresolved",
+ },
+ ],
+ }),
+ headers: {
+ "content-type": "application/json",
+ },
+ method: "POST",
+ },
+ );
+ const verificationResponse = await fetch(
+ `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/verification`,
+ {
+ body: JSON.stringify({
+ verificationNote: "Backup verified the follow-up closure",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "verified",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unverified",
+ },
+ ],
+ }),
+ headers: {
+ "content-type": "application/json",
+ },
+ method: "POST",
+ },
+ );
+ const evidenceResponse = await fetch(
+ `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/evidence`,
+ {
+ body: JSON.stringify({
+ evidenceNote: "Backup collected stable follow-up references",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ references: [
+ "run://queued-follow-up",
+ "note://backup-closeout",
+ ],
+ },
+ {
+ item: "Close backup handoff",
+ references: ["doc://backup-handoff"],
+ },
+ ],
+ }),
+ headers: {
+ "content-type": "application/json",
+ },
+ method: "POST",
+ },
+ );
+ const attestationResponse = await fetch(
+ `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/attestation`,
+ {
+ body: JSON.stringify({
+ attestationNote: "Backup attested the stable follow-up evidence",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "attested",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unattested",
+ },
+ ],
+ }),
+ headers: {
+ "content-type": "application/json",
+ },
+ method: "POST",
+ },
+ );
+ const acknowledgmentResponse = await fetch(
+ `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/acknowledgment`,
+ {
+ body: JSON.stringify({
+ acknowledgmentNote: "Backup acknowledged the attested follow-up",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "acknowledged",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unacknowledged",
+ },
+ ],
+ }),
+ headers: {
+ "content-type": "application/json",
+ },
+ method: "POST",
+ },
+ );
+ const acknowledgedResponse = await fetch(
+ `${peerAddress}/audit/catalog/acknowledged`,
+ );
+ const inspectResponse = await fetch(
+ `${peerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/acknowledgment`,
+ );
+ const applyResponse = await fetch(
+ `${peerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/apply`,
+ );
+ const clearResponse = await fetch(
+ `${ownerAddress}/audit/catalog/${publishedPayload.catalogEntry.entry.id}/acknowledgment/clear`,
+ {
+ method: "POST",
+ },
+ );
+ const acknowledgedAfterClearResponse = await fetch(
+ `${peerAddress}/audit/catalog/acknowledged`,
+ );
+ const acknowledgmentPayload = (await acknowledgmentResponse.json()) as {
+ acknowledgment: {
+ acknowledgment: {
+ acknowledgmentNote?: string;
+ items: Array<{
+ item: string;
+ state: "acknowledged" | "unacknowledged";
+ }>;
+ };
+ attestation: {
+ attestation: {
+ attestationNote?: string;
+ };
+ evidence: {
+ evidence: {
+ evidenceNote?: string;
+ };
+ verification: {
+ verification: {
+ verificationNote?: string;
+ };
+ };
+ };
+ };
+ };
+ };
+ const acknowledgedPayload = (await acknowledgedResponse.json()) as {
+ acknowledged: {
+ items: Array<{
+ acknowledgment: {
+ acknowledgmentNote?: string;
+ };
+ attestation: {
+ evidence: {
+ verification: {
+ resolution: {
+ blocker: {
+ progress: {
+ checklist: {
+ assignment: {
+ review: {
+ visibility: {
+ catalogEntry: {
+ entry: {
+ id: string;
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ }>;
+ totalCount: number;
+ };
+ };
+ const inspectPayload = (await inspectResponse.json()) as {
+ acknowledgment: {
+ acknowledgment: {
+ acknowledgmentNote?: string;
+ items: Array<{
+ item: string;
+ state: "acknowledged" | "unacknowledged";
+ }>;
+ };
+ };
+ };
+ const applyPayload = (await applyResponse.json()) as {
+ application: {
+ application: {
+ navigation: {
+ drilldowns: Array<{
+ result: {
+ runId: string;
+ };
+ }>;
+ totalSummaryCount: number;
+ };
+ savedView: {
+ id: string;
+ };
+ };
+ };
+ };
+ const clearPayload = (await clearResponse.json()) as {
+ acknowledgment: {
+ acknowledgment: {
+ acknowledgmentNote?: string;
+ };
+ };
+ };
+ const acknowledgedAfterClearPayload =
+ (await acknowledgedAfterClearResponse.json()) as {
+ acknowledged: {
+ totalCount: number;
+ };
+ };
+
+ expect(saveResponse.status).toBe(201);
+ expect(publishResponse.status).toBe(201);
+ expect(shareResponse.status).toBe(200);
+ expect(reviewResponse.status).toBe(200);
+ expect(assignmentResponse.status).toBe(200);
+ expect(checklistResponse.status).toBe(200);
+ expect(progressResponse.status).toBe(200);
+ expect(blockerResponse.status).toBe(200);
+ expect(resolutionResponse.status).toBe(200);
+ expect(verificationResponse.status).toBe(200);
+ expect(evidenceResponse.status).toBe(200);
+ expect(attestationResponse.status).toBe(200);
+ expect(acknowledgmentResponse.status).toBe(200);
+ expect(
+ acknowledgmentPayload.acknowledgment.attestation.evidence.verification
+ .verification.verificationNote,
+ ).toBe("Backup verified the follow-up closure");
+ expect(
+ acknowledgmentPayload.acknowledgment.attestation.evidence.evidence
+ .evidenceNote,
+ ).toBe("Backup collected stable follow-up references");
+ expect(
+ acknowledgmentPayload.acknowledgment.attestation.attestation
+ .attestationNote,
+ ).toBe("Backup attested the stable follow-up evidence");
+ expect(
+ acknowledgmentPayload.acknowledgment.acknowledgment.acknowledgmentNote,
+ ).toBe("Backup acknowledged the attested follow-up");
+ expect(acknowledgmentPayload.acknowledgment.acknowledgment.items).toEqual(
+ [
+ {
+ item: "Validate queued follow-up",
+ state: "acknowledged",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unacknowledged",
+ },
+ ],
+ );
+ expect(acknowledgedResponse.status).toBe(200);
+ expect(acknowledgedPayload.acknowledged.totalCount).toBe(1);
+ expect(
+ acknowledgedPayload.acknowledged.items[0]?.attestation.evidence
+ .verification.resolution.blocker.progress.checklist.assignment.review
+ .visibility.catalogEntry.entry.id,
+ ).toBe("catalog_entry_acknowledgment_api");
+ expect(inspectResponse.status).toBe(200);
+ expect(
+ inspectPayload.acknowledgment.acknowledgment.acknowledgmentNote,
+ ).toBe("Backup acknowledged the attested follow-up");
+ expect(applyResponse.status).toBe(200);
+ expect(applyPayload.application.application.savedView.id).toBe(
+ "saved_view_acknowledgment_api",
+ );
+ expect(
+ applyPayload.application.application.navigation.drilldowns[0]?.result
+ .runId,
+ ).toBe(queuedRun.id);
+ expect(clearResponse.status).toBe(200);
+ expect(
+ clearPayload.acknowledgment.acknowledgment.acknowledgmentNote,
+ ).toBe("Backup acknowledged the attested follow-up");
+ expect(acknowledgedAfterClearResponse.status).toBe(200);
+ expect(acknowledgedAfterClearPayload.acknowledged.totalCount).toBe(0);
+ } finally {
+ await ownerApp.close();
+ await peerApp.close();
+ }
+ });
});
diff --git a/apps/api/src/server.test.ts b/apps/api/src/server.test.ts
index eb0f13e..5a1b448 100644
--- a/apps/api/src/server.test.ts
+++ b/apps/api/src/server.test.ts
@@ -25,7 +25,7 @@ describe("@runroot/api", () => {
expect(response.json()).toEqual({
status: "ok",
project: "Runroot",
- phase: 26,
+ phase: 27,
});
});
diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts
index 15f6f4b..c61b7b0 100644
--- a/apps/api/src/server.ts
+++ b/apps/api/src/server.ts
@@ -279,6 +279,12 @@ export function buildServer(options: BuildServerOptions = {}) {
})),
);
+ app.get("/audit/catalog/acknowledged", async (_request, reply) =>
+ handleOperatorResponse(reply, async () => ({
+ acknowledged: await operator.listAcknowledgedCatalogEntries(),
+ })),
+ );
+
app.post("/audit/saved-views", async (request, reply) =>
handleOperatorResponse(reply, async () => {
const body = request.body as {
@@ -512,6 +518,22 @@ export function buildServer(options: BuildServerOptions = {}) {
}),
);
+ app.get(
+ "/audit/catalog/:catalogEntryId/acknowledgment",
+ async (request, reply) =>
+ handleOperatorResponse(reply, async () => {
+ const params = request.params as {
+ readonly catalogEntryId: string;
+ };
+
+ return {
+ acknowledgment: await operator.getCatalogChecklistItemAcknowledgment(
+ params.catalogEntryId,
+ ),
+ };
+ }),
+ );
+
app.get("/audit/catalog/:catalogEntryId", async (request, reply) =>
handleOperatorResponse(reply, async () => {
const params = request.params as {
@@ -812,6 +834,42 @@ export function buildServer(options: BuildServerOptions = {}) {
}),
);
+ app.post(
+ "/audit/catalog/:catalogEntryId/acknowledgment",
+ async (request, reply) =>
+ handleOperatorResponse(reply, async () => {
+ const params = request.params as {
+ readonly catalogEntryId: string;
+ };
+ const body = request.body as {
+ readonly acknowledgmentNote?: string;
+ readonly items?: unknown;
+ };
+ const items = readChecklistItemAcknowledgmentItems(
+ body?.items,
+ "items",
+ );
+
+ if (items.length === 0) {
+ throw new OperatorInputError(
+ "items must include at least one checklist item acknowledgment entry.",
+ );
+ }
+
+ return {
+ acknowledgment: await operator.acknowledgeCatalogEntry(
+ params.catalogEntryId,
+ {
+ ...(body?.acknowledgmentNote !== undefined
+ ? { acknowledgmentNote: body.acknowledgmentNote }
+ : {}),
+ items,
+ },
+ ),
+ };
+ }),
+ );
+
app.post(
"/audit/catalog/:catalogEntryId/review/clear",
async (request, reply) =>
@@ -956,6 +1014,23 @@ export function buildServer(options: BuildServerOptions = {}) {
}),
);
+ app.post(
+ "/audit/catalog/:catalogEntryId/acknowledgment/clear",
+ async (request, reply) =>
+ handleOperatorResponse(reply, async () => {
+ const params = request.params as {
+ readonly catalogEntryId: string;
+ };
+
+ return {
+ acknowledgment:
+ await operator.clearCatalogChecklistItemAcknowledgment(
+ params.catalogEntryId,
+ ),
+ };
+ }),
+ );
+
app.get("/audit/saved-views/:savedViewId/apply", async (request, reply) =>
handleOperatorResponse(reply, async () => {
const params = request.params as {
@@ -1496,3 +1571,30 @@ function readChecklistItemAttestationItems(
return value;
}
+
+function readChecklistItemAcknowledgmentItems(
+ value: unknown,
+ fieldName: string,
+): readonly {
+ readonly item: string;
+ readonly state: "acknowledged" | "unacknowledged";
+}[] {
+ if (
+ !Array.isArray(value) ||
+ !value.every(
+ (entry) =>
+ typeof entry === "object" &&
+ entry !== null &&
+ "item" in entry &&
+ typeof entry.item === "string" &&
+ "state" in entry &&
+ (entry.state === "acknowledged" || entry.state === "unacknowledged"),
+ )
+ ) {
+ throw new OperatorInputError(
+ `${fieldName} must be an array of { item, state } objects with state acknowledged|unacknowledged.`,
+ );
+ }
+
+ return value;
+}
diff --git a/apps/web/src/app/runs/catalog/route.ts b/apps/web/src/app/runs/catalog/route.ts
index d7f2483..c1c276c 100644
--- a/apps/web/src/app/runs/catalog/route.ts
+++ b/apps/web/src/app/runs/catalog/route.ts
@@ -504,6 +504,53 @@ export async function POST(request: Request) {
return Response.redirect(redirectUrl, 303);
}
+ if (intent === "acknowledge") {
+ const catalogEntryId = readTrimmedFormValue(formData, "catalogEntryId");
+ const acknowledgmentItems = readChecklistItemAcknowledgmentItems(
+ formData.get("acknowledgmentItems"),
+ );
+ const acknowledgmentNoteValue = formData.get("acknowledgmentNote");
+
+ if (!catalogEntryId) {
+ appendFlashMessage(
+ redirectUrl,
+ "error",
+ "A catalog entry id is required to update checklist item acknowledgment metadata.",
+ );
+
+ return Response.redirect(redirectUrl, 303);
+ }
+
+ if (acknowledgmentItems.length === 0) {
+ appendFlashMessage(
+ redirectUrl,
+ "error",
+ "At least one checklist item acknowledgment entry is required.",
+ );
+
+ return Response.redirect(redirectUrl, 303);
+ }
+
+ const acknowledgment =
+ await createRunrootApiClient().setAuditCatalogChecklistItemAcknowledgment(
+ catalogEntryId,
+ {
+ ...(typeof acknowledgmentNoteValue === "string"
+ ? { acknowledgmentNote: acknowledgmentNoteValue }
+ : {}),
+ items: acknowledgmentItems,
+ },
+ );
+
+ appendFlashMessage(
+ redirectUrl,
+ "notice",
+ `Checklist item acknowledgments for ${acknowledgment.attestation.evidence.verification.resolution.blocker.progress.checklist.assignment.review.visibility.catalogEntry.entry.name} updated.`,
+ );
+
+ return Response.redirect(redirectUrl, 303);
+ }
+
if (intent === "clear-review") {
const catalogEntryId = readTrimmedFormValue(formData, "catalogEntryId");
@@ -693,6 +740,33 @@ export async function POST(request: Request) {
return Response.redirect(redirectUrl, 303);
}
+ if (intent === "clear-acknowledgment") {
+ const catalogEntryId = readTrimmedFormValue(formData, "catalogEntryId");
+
+ if (!catalogEntryId) {
+ appendFlashMessage(
+ redirectUrl,
+ "error",
+ "A catalog entry id is required to clear checklist item acknowledgment metadata.",
+ );
+
+ return Response.redirect(redirectUrl, 303);
+ }
+
+ const acknowledgment =
+ await createRunrootApiClient().clearAuditCatalogChecklistItemAcknowledgment(
+ catalogEntryId,
+ );
+
+ appendFlashMessage(
+ redirectUrl,
+ "notice",
+ `Checklist item acknowledgments for ${acknowledgment.attestation.evidence.verification.resolution.blocker.progress.checklist.assignment.review.visibility.catalogEntry.entry.name} cleared.`,
+ );
+
+ return Response.redirect(redirectUrl, 303);
+ }
+
if (intent === "clear-progress") {
const catalogEntryId = readTrimmedFormValue(formData, "catalogEntryId");
@@ -1096,3 +1170,49 @@ function readChecklistItemAttestationItems(
};
});
}
+
+function readChecklistItemAcknowledgmentItems(
+ value: FormDataEntryValue | null,
+): readonly {
+ readonly item: string;
+ readonly state: "acknowledged" | "unacknowledged";
+}[] {
+ if (typeof value !== "string") {
+ return [];
+ }
+
+ return value
+ .split(/\r?\n/u)
+ .map((line) => line.trim())
+ .filter((line) => line.length > 0)
+ .map((line) => {
+ const separatorIndex = line.indexOf(":");
+
+ if (separatorIndex < 0) {
+ return {
+ item: line,
+ state: "unacknowledged" as const,
+ };
+ }
+
+ const rawState = line.slice(0, separatorIndex).trim();
+ const item = line.slice(separatorIndex + 1).trim();
+
+ if (item.length === 0) {
+ throw new Error(
+ "Checklist item acknowledgment lines require a non-empty item.",
+ );
+ }
+
+ if (rawState !== "acknowledged" && rawState !== "unacknowledged") {
+ throw new Error(
+ `Checklist item acknowledgment state must be acknowledged or unacknowledged for "${item}".`,
+ );
+ }
+
+ return {
+ item,
+ state: rawState,
+ };
+ });
+}
diff --git a/apps/web/src/app/runs/page.tsx b/apps/web/src/app/runs/page.tsx
index 5a9be54..6c4efeb 100644
--- a/apps/web/src/app/runs/page.tsx
+++ b/apps/web/src/app/runs/page.tsx
@@ -3,6 +3,7 @@ import {
AuditViewCatalogsView,
CatalogReviewAssignmentsView,
CatalogReviewSignalsView,
+ ChecklistItemAcknowledgmentsView,
ChecklistItemAttestationsView,
ChecklistItemBlockersView,
ChecklistItemEvidencesView,
@@ -23,6 +24,7 @@ import {
} from "../../lib/navigation";
import {
type ApiAuditCatalogAssignmentChecklistView,
+ type ApiAuditCatalogChecklistItemAcknowledgmentView,
type ApiAuditCatalogChecklistItemAttestationView,
type ApiAuditCatalogChecklistItemBlockerView,
type ApiAuditCatalogChecklistItemEvidenceView,
@@ -62,6 +64,7 @@ export default async function RunsPage({
const drilldownFilters = readAuditDrilldownFilters(resolvedSearchParams);
const [
runs,
+ acknowledgedEntries,
blockedEntries,
evidencedEntries,
attestedEntries,
@@ -78,6 +81,7 @@ export default async function RunsPage({
catalogChecklistItemBlocker,
catalogChecklistItemResolution,
catalogChecklistItemEvidence,
+ catalogChecklistItemAcknowledgment,
catalogChecklistItemAttestation,
catalogChecklistItemVerification,
catalogChecklistItemProgress,
@@ -86,6 +90,7 @@ export default async function RunsPage({
catalogReviewAssignment,
] = await Promise.all([
api.listRuns(),
+ api.listAcknowledgedAuditCatalogEntries(),
api.listBlockedAuditCatalogEntries(),
api.listEvidencedAuditCatalogEntries(),
api.listAttestedAuditCatalogEntries(),
@@ -123,6 +128,11 @@ export default async function RunsPage({
.getAuditCatalogChecklistItemEvidence(catalogEntryId)
.catch(() => undefined)
: Promise.resolve(undefined),
+ catalogEntryId
+ ? api
+ .getAuditCatalogChecklistItemAcknowledgment(catalogEntryId)
+ .catch(() => undefined)
+ : Promise.resolve(undefined),
catalogEntryId
? api
.getAuditCatalogChecklistItemAttestation(catalogEntryId)
@@ -166,6 +176,9 @@ export default async function RunsPage({
let activeCatalogChecklistItemEvidence:
| ApiAuditCatalogChecklistItemEvidenceView
| undefined;
+ let activeCatalogChecklistItemAcknowledgment:
+ | ApiAuditCatalogChecklistItemAcknowledgmentView
+ | undefined;
let activeCatalogChecklistItemAttestation:
| ApiAuditCatalogChecklistItemAttestationView
| undefined;
@@ -187,6 +200,8 @@ export default async function RunsPage({
activeCatalogChecklistItemBlocker = catalogChecklistItemBlocker;
activeCatalogChecklistItemResolution = catalogChecklistItemResolution;
activeCatalogChecklistItemEvidence = catalogChecklistItemEvidence;
+ activeCatalogChecklistItemAcknowledgment =
+ catalogChecklistItemAcknowledgment;
activeCatalogChecklistItemAttestation = catalogChecklistItemAttestation;
activeCatalogChecklistItemVerification = catalogChecklistItemVerification;
activeCatalogChecklistItemProgress = catalogChecklistItemProgress;
@@ -226,6 +241,12 @@ export default async function RunsPage({
? { activeCatalogChecklistItemEvidence }
: {})}
/>
+
{
await peerApp.close();
}
});
+
+ it("renders, records, clears, and reapplies checklist item acknowledgments through the existing API surface", async () => {
+ const workspaceRoot = await mkdtemp(
+ join(tmpdir(), "runroot-web-checklist-acknowledgment-"),
+ );
+ const sqlitePath = join(workspaceRoot, "runroot.sqlite");
+ const inlineOperator = createRunrootOperatorService({
+ executionMode: "inline",
+ operatorId: "ops_oncall",
+ operatorScopeId: "ops",
+ persistenceDriver: "sqlite",
+ sqlitePath,
+ });
+ const queuedOperator = createRunrootOperatorService({
+ executionMode: "queued",
+ persistenceDriver: "sqlite",
+ sqlitePath,
+ });
+ const workerService = (
+ await import("@runroot/sdk")
+ ).createRunrootWorkerService({
+ persistenceDriver: "sqlite",
+ sqlitePath,
+ workerId: "worker_web_acknowledgment",
+ });
+ const ownerApp = buildServer({
+ operator: createRunrootOperatorService({
+ catalogEntryIdGenerator: () => "catalog_entry_acknowledgment_web",
+ operatorId: "ops_oncall",
+ operatorScopeId: "ops",
+ persistenceDriver: "sqlite",
+ savedViewIdGenerator: () => "saved_view_acknowledgment_web",
+ sqlitePath,
+ }),
+ });
+ const peerApp = buildServer({
+ operator: createRunrootOperatorService({
+ operatorId: "ops_backup",
+ operatorScopeId: "ops",
+ persistenceDriver: "sqlite",
+ sqlitePath,
+ }),
+ });
+
+ try {
+ const inlineRun = await inlineOperator.startRun({
+ input: {
+ approvalRequired: false,
+ commandAlias: "print-ready",
+ runbookId: "node-health-check",
+ },
+ templateId: "shell-runbook-flow",
+ });
+ const queuedRun = await queuedOperator.startRun({
+ input: {
+ approvalRequired: false,
+ commandAlias: "print-ready",
+ runbookId: "node-health-check",
+ },
+ templateId: "shell-runbook-flow",
+ });
+
+ await workerService.processNextJob();
+
+ const ownerAddress = await ownerApp.listen({
+ host: "127.0.0.1",
+ port: 0,
+ });
+ const peerAddress = await peerApp.listen({
+ host: "127.0.0.1",
+ port: 0,
+ });
+
+ process.env.RUNROOT_API_BASE_URL = ownerAddress;
+
+ const saveForm = new FormData();
+ saveForm.set("name", "Queued acknowledgment preset");
+ saveForm.set("description", "Saved queued worker acknowledgment preset");
+ saveForm.set("summaryExecutionMode", "queued");
+ saveForm.set("drilldownWorkerId", "worker_web_acknowledgment");
+ saveForm.set("auditViewRunId", queuedRun.id);
+ saveForm.set("drilldownRunId", queuedRun.id);
+ saveForm.set("returnTo", "/runs");
+
+ const saveResponse = await saveSavedView(
+ new Request("http://localhost/runs/saved-views", {
+ body: saveForm,
+ method: "POST",
+ }),
+ );
+
+ expect(saveResponse.status).toBe(303);
+
+ const publishForm = new FormData();
+ publishForm.set("intent", "publish");
+ publishForm.set("savedViewId", "saved_view_acknowledgment_web");
+
+ const publishResponse = await mutateCatalog(
+ new Request("http://localhost/runs/catalog", {
+ body: publishForm,
+ method: "POST",
+ }),
+ );
+
+ expect(publishResponse.status).toBe(303);
+
+ const shareForm = new FormData();
+ shareForm.set("catalogEntryId", "catalog_entry_acknowledgment_web");
+ shareForm.set("intent", "share");
+
+ const shareResponse = await mutateCatalog(
+ new Request("http://localhost/runs/catalog", {
+ body: shareForm,
+ method: "POST",
+ }),
+ );
+
+ expect(shareResponse.status).toBe(303);
+
+ const reviewForm = new FormData();
+ reviewForm.set("catalogEntryId", "catalog_entry_acknowledgment_web");
+ reviewForm.set("intent", "review");
+ reviewForm.set(
+ "note",
+ `Acknowledgment ready after inline ${inlineRun.id}`,
+ );
+ reviewForm.set("reviewState", "recommended");
+ reviewForm.set(
+ "returnTo",
+ "/runs?catalogEntryId=catalog_entry_acknowledgment_web",
+ );
+
+ const reviewResponse = await mutateCatalog(
+ new Request("http://localhost/runs/catalog", {
+ body: reviewForm,
+ method: "POST",
+ }),
+ );
+
+ expect(reviewResponse.status).toBe(303);
+
+ const assignForm = new FormData();
+ assignForm.set("catalogEntryId", "catalog_entry_acknowledgment_web");
+ assignForm.set("intent", "assign");
+ assignForm.set("assigneeId", "ops_backup");
+ assignForm.set(
+ "handoffNote",
+ `Queued worker ${queuedRun.id} handed to backup`,
+ );
+ assignForm.set(
+ "returnTo",
+ "/runs?catalogEntryId=catalog_entry_acknowledgment_web",
+ );
+
+ const assignResponse = await mutateCatalog(
+ new Request("http://localhost/runs/catalog", {
+ body: assignForm,
+ method: "POST",
+ }),
+ );
+
+ expect(assignResponse.status).toBe(303);
+
+ const checklistForm = new FormData();
+ checklistForm.set("catalogEntryId", "catalog_entry_acknowledgment_web");
+ checklistForm.set("intent", "checklist");
+ checklistForm.set("checklistState", "pending");
+ checklistForm.set(
+ "checklistItems",
+ "Validate queued follow-up\nClose backup handoff",
+ );
+ checklistForm.set(
+ "returnTo",
+ "/runs?catalogEntryId=catalog_entry_acknowledgment_web",
+ );
+
+ const checklistResponse = await mutateCatalog(
+ new Request("http://localhost/runs/catalog", {
+ body: checklistForm,
+ method: "POST",
+ }),
+ );
+
+ expect(checklistResponse.status).toBe(303);
+
+ const progressForm = new FormData();
+ progressForm.set("catalogEntryId", "catalog_entry_acknowledgment_web");
+ progressForm.set("intent", "progress");
+ progressForm.set(
+ "progressItems",
+ "completed: Validate queued follow-up\npending: Close backup handoff",
+ );
+ progressForm.set("completionNote", "Queued follow-up is almost complete");
+ progressForm.set(
+ "returnTo",
+ "/runs?catalogEntryId=catalog_entry_acknowledgment_web",
+ );
+
+ const progressResponse = await mutateCatalog(
+ new Request("http://localhost/runs/catalog", {
+ body: progressForm,
+ method: "POST",
+ }),
+ );
+
+ expect(progressResponse.status).toBe(303);
+
+ const blockerForm = new FormData();
+ blockerForm.set("catalogEntryId", "catalog_entry_acknowledgment_web");
+ blockerForm.set("intent", "block");
+ blockerForm.set(
+ "blockerItems",
+ "cleared: Validate queued follow-up\nblocked: Close backup handoff",
+ );
+ blockerForm.set("blockerNote", "Waiting for the overnight handoff");
+ blockerForm.set(
+ "returnTo",
+ "/runs?catalogEntryId=catalog_entry_acknowledgment_web",
+ );
+
+ const blockerResponse = await mutateCatalog(
+ new Request("http://localhost/runs/catalog", {
+ body: blockerForm,
+ method: "POST",
+ }),
+ );
+
+ expect(blockerResponse.status).toBe(303);
+
+ const resolutionForm = new FormData();
+ resolutionForm.set("catalogEntryId", "catalog_entry_acknowledgment_web");
+ resolutionForm.set("intent", "resolve");
+ resolutionForm.set(
+ "resolutionItems",
+ "resolved: Validate queued follow-up\nunresolved: Close backup handoff",
+ );
+ resolutionForm.set(
+ "resolutionNote",
+ "Backup confirmed the follow-up closure",
+ );
+ resolutionForm.set(
+ "returnTo",
+ "/runs?catalogEntryId=catalog_entry_acknowledgment_web",
+ );
+
+ const resolutionResponse = await mutateCatalog(
+ new Request("http://localhost/runs/catalog", {
+ body: resolutionForm,
+ method: "POST",
+ }),
+ );
+
+ expect(resolutionResponse.status).toBe(303);
+
+ const verificationForm = new FormData();
+ verificationForm.set(
+ "catalogEntryId",
+ "catalog_entry_acknowledgment_web",
+ );
+ verificationForm.set("intent", "verify");
+ verificationForm.set(
+ "verificationItems",
+ "verified: Validate queued follow-up\nunverified: Close backup handoff",
+ );
+ verificationForm.set(
+ "verificationNote",
+ "Backup verified the follow-up closure",
+ );
+ verificationForm.set(
+ "returnTo",
+ "/runs?catalogEntryId=catalog_entry_acknowledgment_web",
+ );
+
+ const verificationResponse = await mutateCatalog(
+ new Request("http://localhost/runs/catalog", {
+ body: verificationForm,
+ method: "POST",
+ }),
+ );
+
+ expect(verificationResponse.status).toBe(303);
+
+ const evidenceForm = new FormData();
+ evidenceForm.set("catalogEntryId", "catalog_entry_acknowledgment_web");
+ evidenceForm.set("intent", "record-evidence");
+ evidenceForm.set(
+ "evidenceItems",
+ "Validate queued follow-up: run://queued-follow-up | note://backup-closeout\nClose backup handoff: doc://backup-handoff",
+ );
+ evidenceForm.set(
+ "evidenceNote",
+ "Backup collected stable follow-up references",
+ );
+ evidenceForm.set(
+ "returnTo",
+ "/runs?catalogEntryId=catalog_entry_acknowledgment_web",
+ );
+
+ const evidenceResponse = await mutateCatalog(
+ new Request("http://localhost/runs/catalog", {
+ body: evidenceForm,
+ method: "POST",
+ }),
+ );
+
+ expect(evidenceResponse.status).toBe(303);
+
+ const attestationForm = new FormData();
+ attestationForm.set("catalogEntryId", "catalog_entry_acknowledgment_web");
+ attestationForm.set("intent", "attest");
+ attestationForm.set(
+ "attestationItems",
+ "attested: Validate queued follow-up\nunattested: Close backup handoff",
+ );
+ attestationForm.set(
+ "attestationNote",
+ "Backup attested the stable follow-up evidence",
+ );
+ attestationForm.set(
+ "returnTo",
+ "/runs?catalogEntryId=catalog_entry_acknowledgment_web",
+ );
+
+ const attestationResponse = await mutateCatalog(
+ new Request("http://localhost/runs/catalog", {
+ body: attestationForm,
+ method: "POST",
+ }),
+ );
+
+ expect(attestationResponse.status).toBe(303);
+
+ const acknowledgmentForm = new FormData();
+ acknowledgmentForm.set(
+ "catalogEntryId",
+ "catalog_entry_acknowledgment_web",
+ );
+ acknowledgmentForm.set("intent", "acknowledge");
+ acknowledgmentForm.set(
+ "acknowledgmentItems",
+ "acknowledged: Validate queued follow-up\nunacknowledged: Close backup handoff",
+ );
+ acknowledgmentForm.set(
+ "acknowledgmentNote",
+ "Backup acknowledged the attested follow-up",
+ );
+ acknowledgmentForm.set(
+ "returnTo",
+ "/runs?catalogEntryId=catalog_entry_acknowledgment_web",
+ );
+
+ const acknowledgmentResponse = await mutateCatalog(
+ new Request("http://localhost/runs/catalog", {
+ body: acknowledgmentForm,
+ method: "POST",
+ }),
+ );
+
+ expect(acknowledgmentResponse.status).toBe(303);
+
+ process.env.RUNROOT_API_BASE_URL = peerAddress;
+
+ const acknowledgedMarkup = renderToStaticMarkup(
+ await RunsPage({
+ searchParams: Promise.resolve({
+ catalogEntryId: "catalog_entry_acknowledgment_web",
+ }),
+ }),
+ );
+
+ expect(acknowledgedMarkup).toContain("Checklist item acknowledgments");
+ expect(acknowledgedMarkup).toContain("Queued acknowledgment preset");
+ expect(acknowledgedMarkup).toContain("1/2 acknowledged");
+ expect(acknowledgedMarkup).toContain("Validate queued follow-up");
+ expect(acknowledgedMarkup).toContain("Close backup handoff");
+ expect(acknowledgedMarkup).toContain(
+ "Backup acknowledged the attested follow-up",
+ );
+ expect(acknowledgedMarkup).toContain("Apply acknowledged preset");
+ expect(acknowledgedMarkup).toContain(
+ `Queued worker ${queuedRun.id} handed to backup`,
+ );
+ expect(acknowledgedMarkup).toContain("Active acknowledgments selected");
+
+ process.env.RUNROOT_API_BASE_URL = ownerAddress;
+
+ const clearForm = new FormData();
+ clearForm.set("catalogEntryId", "catalog_entry_acknowledgment_web");
+ clearForm.set("intent", "clear-acknowledgment");
+ clearForm.set(
+ "returnTo",
+ "/runs?catalogEntryId=catalog_entry_acknowledgment_web",
+ );
+
+ const clearResponse = await mutateCatalog(
+ new Request("http://localhost/runs/catalog", {
+ body: clearForm,
+ method: "POST",
+ }),
+ );
+
+ expect(clearResponse.status).toBe(303);
+
+ process.env.RUNROOT_API_BASE_URL = peerAddress;
+
+ const clearedMarkup = renderToStaticMarkup(
+ await RunsPage({
+ searchParams: Promise.resolve({
+ catalogEntryId: "catalog_entry_acknowledgment_web",
+ }),
+ }),
+ );
+
+ expect(clearedMarkup).toContain("Checklist item acknowledgments");
+ expect(clearedMarkup).toContain("No checklist item acknowledgments yet");
+ expect(clearedMarkup).not.toContain(
+ "Backup acknowledged the attested follow-up",
+ );
+ } finally {
+ await ownerApp.close();
+ await peerApp.close();
+ }
+ });
});
diff --git a/apps/web/src/components/console.tsx b/apps/web/src/components/console.tsx
index 844dc20..6db2d9d 100644
--- a/apps/web/src/components/console.tsx
+++ b/apps/web/src/components/console.tsx
@@ -4,6 +4,9 @@ import type {
ApiApproval,
ApiAuditCatalogAssignmentChecklistCollection,
ApiAuditCatalogAssignmentChecklistView,
+ ApiAuditCatalogChecklistItemAcknowledgmentCollection,
+ ApiAuditCatalogChecklistItemAcknowledgmentItem,
+ ApiAuditCatalogChecklistItemAcknowledgmentView,
ApiAuditCatalogChecklistItemAttestationCollection,
ApiAuditCatalogChecklistItemAttestationItem,
ApiAuditCatalogChecklistItemAttestationView,
@@ -1210,6 +1213,171 @@ export function ChecklistItemAttestationsView({
);
}
+export function ChecklistItemAcknowledgmentsView({
+ activeCatalogChecklistItemAcknowledgment,
+ acknowledgedEntries,
+}: Readonly<{
+ activeCatalogChecklistItemAcknowledgment?: ApiAuditCatalogChecklistItemAcknowledgmentView;
+ acknowledgedEntries: ApiAuditCatalogChecklistItemAcknowledgmentCollection;
+}>) {
+ return (
+
+
+
+
+ Phase 27 / Checklist Item Acknowledgments
+
+
Checklist item acknowledgments
+
+ Track thin per-item acknowledgments and a single acknowledgment note
+ on attested presets without turning the console into an approval
+ product, workflow engine, or collaboration surface.
+
+
+
+ {acknowledgedEntries.totalCount} acknowledged preset(s)
+
+
+
+ {activeCatalogChecklistItemAcknowledgment ? (
+
+ Active acknowledgments:{" "}
+
+ {
+ activeCatalogChecklistItemAcknowledgment.attestation.evidence
+ .verification.resolution.blocker.progress.checklist.assignment
+ .review.visibility.catalogEntry.entry.name
+ }
+
+ {" · "}
+ {formatAcknowledgmentSummary(
+ activeCatalogChecklistItemAcknowledgment.acknowledgment.items,
+ )}
+ {activeCatalogChecklistItemAcknowledgment.acknowledgment
+ .acknowledgmentNote
+ ? ` · ${activeCatalogChecklistItemAcknowledgment.acknowledgment.acknowledgmentNote}`
+ : ""}
+
+ ) : null}
+
+ {acknowledgedEntries.items.length === 0 ? (
+
+ No checklist item acknowledgments yet. Record attestations first, then
+ save thin acknowledgment metadata through the shared operator seam.
+
+ ) : (
+
+ {acknowledgedEntries.items.map((acknowledgmentView) => (
+
+
+
+
+ {
+ acknowledgmentView.attestation.evidence.verification
+ .resolution.blocker.progress.checklist.assignment.review
+ .visibility.catalogEntry.entry.name
+ }
+
+
+ {formatAcknowledgmentSummary(
+ acknowledgmentView.acknowledgment.items,
+ )}
+ {" · "}
+ {formatAttestationSummary(
+ acknowledgmentView.attestation.attestation.items,
+ )}
+ {" · "}
+ {formatEvidenceSummary(
+ acknowledgmentView.attestation.evidence.evidence.items,
+ )}
+
+
+
+ {formatTimestamp(acknowledgmentView.acknowledgment.updatedAt)}
+
+
+
+ {acknowledgmentView.acknowledgment.items.map((item) => (
+
+ {item.state} : {item.item}
+
+ ))}
+
+ {acknowledgmentView.acknowledgment.acknowledgmentNote ? (
+
+ {acknowledgmentView.acknowledgment.acknowledgmentNote}
+
+ ) : null}
+
+ operator {acknowledgmentView.acknowledgment.operatorId} · scope{" "}
+ {acknowledgmentView.acknowledgment.scopeId}
+
+
+ {activeCatalogChecklistItemAcknowledgment?.acknowledgment
+ .catalogEntryId ===
+ acknowledgmentView.acknowledgment.catalogEntryId ? (
+
+ Active acknowledgments selected
+
+ ) : null}
+
+ ))}
+
+ )}
+
+ );
+}
+
export function CatalogReviewAssignmentsView({
activeCatalogReviewAssignment,
assignedEntries,
@@ -1445,12 +1613,14 @@ export function CatalogReviewSignalsView({
export function AuditViewCatalogsView({
activeCatalogEntry,
activeCatalogChecklistItemBlocker,
+ activeCatalogChecklistItemAcknowledgment,
activeCatalogChecklistItemAttestation,
activeCatalogChecklistItemEvidence,
activeCatalogChecklistItemResolution,
activeCatalogChecklistItemVerification,
activeCatalogChecklistItemProgress,
assignedEntries,
+ acknowledgedEntries,
attestedEntries,
blockedEntries,
catalogEntries,
@@ -1463,12 +1633,14 @@ export function AuditViewCatalogsView({
}: Readonly<{
activeCatalogEntry?: ApiAuditCatalogVisibilityView;
activeCatalogChecklistItemBlocker?: ApiAuditCatalogChecklistItemBlockerView;
+ activeCatalogChecklistItemAcknowledgment?: ApiAuditCatalogChecklistItemAcknowledgmentView;
activeCatalogChecklistItemAttestation?: ApiAuditCatalogChecklistItemAttestationView;
activeCatalogChecklistItemEvidence?: ApiAuditCatalogChecklistItemEvidenceView;
activeCatalogChecklistItemResolution?: ApiAuditCatalogChecklistItemResolutionView;
activeCatalogChecklistItemVerification?: ApiAuditCatalogChecklistItemVerificationView;
activeCatalogChecklistItemProgress?: ApiAuditCatalogChecklistItemProgressView;
assignedEntries: ApiAuditCatalogReviewAssignmentCollection;
+ acknowledgedEntries: ApiAuditCatalogChecklistItemAcknowledgmentCollection;
attestedEntries: ApiAuditCatalogChecklistItemAttestationCollection;
blockedEntries: ApiAuditCatalogChecklistItemBlockerCollection;
catalogEntries: ApiAuditCatalogVisibilityCollection;
@@ -1557,6 +1729,16 @@ export function AuditViewCatalogsView({
] as const,
),
);
+ const acknowledgmentsByCatalogEntryId = new Map(
+ acknowledgedEntries.items.map(
+ (item) =>
+ [
+ item.attestation.evidence.verification.resolution.blocker.progress
+ .checklist.assignment.review.visibility.catalogEntry.entry.id,
+ item,
+ ] as const,
+ ),
+ );
return (
@@ -1634,6 +1816,15 @@ export function AuditViewCatalogsView({
: attestationsByCatalogEntryId.get(
catalogEntry.catalogEntry.entry.id,
);
+ const acknowledgment =
+ activeCatalogChecklistItemAcknowledgment?.attestation.evidence
+ .verification.resolution.blocker.progress.checklist
+ .assignment.review.visibility.catalogEntry.entry.id ===
+ catalogEntry.catalogEntry.entry.id
+ ? activeCatalogChecklistItemAcknowledgment
+ : acknowledgmentsByCatalogEntryId.get(
+ catalogEntry.catalogEntry.entry.id,
+ );
const reviewSignal = reviewSignalsByCatalogEntryId.get(
catalogEntry.catalogEntry.entry.id,
);
@@ -1783,6 +1974,20 @@ export function AuditViewCatalogsView({
{attestation.attestation.attestationNote}
) : null}
+ {acknowledgment?.acknowledgment.items.length ? (
+
+ acknowledgments:{" "}
+ {acknowledgment.acknowledgment.items
+ .map((item) => `${item.state}: ${item.item}`)
+ .join(", ")}
+
+ ) : null}
+ {acknowledgment?.acknowledgment.acknowledgmentNote ? (
+
+ acknowledgment note:{" "}
+ {acknowledgment.acknowledgment.acknowledgmentNote}
+
+ ) : null}
saved view {catalogEntry.catalogEntry.savedView.id}
{catalogEntry.catalogEntry.savedView.refs.auditViewRunId
@@ -1822,6 +2027,9 @@ export function AuditViewCatalogsView({
{attestation
? ` · attestation owner ${attestation.attestation.operatorId}`
: ""}
+ {acknowledgment
+ ? ` · acknowledgment owner ${acknowledgment.acknowledgment.operatorId}`
+ : ""}
summary filters{" "}
@@ -2313,6 +2521,66 @@ export function AuditViewCatalogsView({
) : null}
+ {attestation ? (
+
+ ) : null}
Clear attestations
) : null}
+ {acknowledgment ? (
+
+
+
+
+ Clear acknowledgments
+
+ ) : null}
) : null}
+ {activeCatalogChecklistItemAcknowledgment?.attestation
+ .evidence.verification.resolution.blocker.progress
+ .checklist.assignment.review.visibility.catalogEntry.entry
+ .id === catalogEntry.catalogEntry.entry.id ? (
+
+ Active acknowledgments selected
+
+ ) : null}
>
);
})()}
@@ -4009,6 +4311,16 @@ function formatAttestationSummary(
return `${attestedCount}/${items.length} attested`;
}
+function formatAcknowledgmentSummary(
+ items: readonly ApiAuditCatalogChecklistItemAcknowledgmentItem[],
+): string {
+ const acknowledgedCount = items.filter(
+ (item) => item.state === "acknowledged",
+ ).length;
+
+ return `${acknowledgedCount}/${items.length} acknowledged`;
+}
+
function formatChecklistItemProgressLines(
checklistItems: readonly string[],
progressItems:
@@ -4110,6 +4422,24 @@ function formatChecklistItemAttestationLines(
.join("\n");
}
+function formatChecklistItemAcknowledgmentLines(
+ attestationItems: readonly ApiAuditCatalogChecklistItemAttestationItem[],
+ acknowledgmentItems:
+ | readonly ApiAuditCatalogChecklistItemAcknowledgmentItem[]
+ | undefined,
+): string {
+ const acknowledgmentByItem = new Map(
+ (acknowledgmentItems ?? []).map((item) => [item.item, item.state] as const),
+ );
+
+ return attestationItems
+ .map(
+ (item) =>
+ `${acknowledgmentByItem.get(item.item) ?? "unacknowledged"}: ${item.item}`,
+ )
+ .join("\n");
+}
+
function buildSavedAuditViewHref(savedViewId: string): string {
return `/runs?savedViewId=${encodeURIComponent(savedViewId)}`;
}
diff --git a/apps/web/src/lib/runroot-api.ts b/apps/web/src/lib/runroot-api.ts
index 167f7cf..cb01fa0 100644
--- a/apps/web/src/lib/runroot-api.ts
+++ b/apps/web/src/lib/runroot-api.ts
@@ -558,6 +558,32 @@ export interface ApiAuditCatalogChecklistItemEvidenceCollection {
readonly totalCount: number;
}
+export interface ApiAuditCatalogChecklistItemAcknowledgmentItem {
+ readonly item: string;
+ readonly state: "acknowledged" | "unacknowledged";
+}
+
+export interface ApiAuditCatalogChecklistItemAcknowledgment {
+ readonly acknowledgmentNote?: string;
+ readonly catalogEntryId: string;
+ readonly createdAt: string;
+ readonly items: readonly ApiAuditCatalogChecklistItemAcknowledgmentItem[];
+ readonly kind: "catalog-checklist-item-acknowledgment";
+ readonly operatorId: string;
+ readonly scopeId: string;
+ readonly updatedAt: string;
+}
+
+export interface ApiAuditCatalogChecklistItemAcknowledgmentView {
+ readonly acknowledgment: ApiAuditCatalogChecklistItemAcknowledgment;
+ readonly attestation: ApiAuditCatalogChecklistItemAttestationView;
+}
+
+export interface ApiAuditCatalogChecklistItemAcknowledgmentCollection {
+ readonly items: readonly ApiAuditCatalogChecklistItemAcknowledgmentView[];
+ readonly totalCount: number;
+}
+
export interface ApiAuditCatalogChecklistItemAttestationItem {
readonly item: string;
readonly state: "attested" | "unattested";
@@ -625,6 +651,11 @@ export interface UpdateApiAuditCatalogChecklistItemAttestationInput {
readonly items: readonly ApiAuditCatalogChecklistItemAttestationItem[];
}
+export interface UpdateApiAuditCatalogChecklistItemAcknowledgmentInput {
+ readonly acknowledgmentNote?: string;
+ readonly items: readonly ApiAuditCatalogChecklistItemAcknowledgmentItem[];
+}
+
export interface UpdateApiAuditCatalogReviewAssignmentInput {
readonly assigneeId: string;
readonly handoffNote?: string;
@@ -673,6 +704,9 @@ export interface RunrootApiClient {
clearAuditCatalogChecklistItemEvidence(
catalogEntryId: string,
): Promise;
+ clearAuditCatalogChecklistItemAcknowledgment(
+ catalogEntryId: string,
+ ): Promise;
clearAuditCatalogChecklistItemAttestation(
catalogEntryId: string,
): Promise;
@@ -703,6 +737,9 @@ export interface RunrootApiClient {
getAuditCatalogChecklistItemEvidence(
catalogEntryId: string,
): Promise;
+ getAuditCatalogChecklistItemAcknowledgment(
+ catalogEntryId: string,
+ ): Promise;
getAuditCatalogChecklistItemAttestation(
catalogEntryId: string,
): Promise;
@@ -738,6 +775,7 @@ export interface RunrootApiClient {
getSavedAuditView(savedViewId: string): Promise;
listAssignedAuditCatalogEntries(): Promise;
listBlockedAuditCatalogEntries(): Promise;
+ listAcknowledgedAuditCatalogEntries(): Promise;
listEvidencedAuditCatalogEntries(): Promise;
listAttestedAuditCatalogEntries(): Promise;
listResolvedAuditCatalogEntries(): Promise;
@@ -773,6 +811,10 @@ export interface RunrootApiClient {
catalogEntryId: string,
input: UpdateApiAuditCatalogChecklistItemEvidenceInput,
): Promise;
+ setAuditCatalogChecklistItemAcknowledgment(
+ catalogEntryId: string,
+ input: UpdateApiAuditCatalogChecklistItemAcknowledgmentInput,
+ ): Promise;
setAuditCatalogChecklistItemAttestation(
catalogEntryId: string,
input: UpdateApiAuditCatalogChecklistItemAttestationInput,
@@ -1013,6 +1055,18 @@ export function createRunrootApiClient(
return payload.evidence;
},
+ async clearAuditCatalogChecklistItemAcknowledgment(catalogEntryId) {
+ const payload = await requestJson<{
+ acknowledgment: ApiAuditCatalogChecklistItemAcknowledgmentView;
+ }>(
+ `/audit/catalog/${catalogEntryId}/acknowledgment/clear`,
+ { method: "POST" },
+ "web.api.clearAuditCatalogChecklistItemAcknowledgment",
+ );
+
+ return payload.acknowledgment;
+ },
+
async clearAuditCatalogChecklistItemAttestation(catalogEntryId) {
const payload = await requestJson<{
attestation: ApiAuditCatalogChecklistItemAttestationView;
@@ -1133,6 +1187,18 @@ export function createRunrootApiClient(
return payload.evidence;
},
+ async getAuditCatalogChecklistItemAcknowledgment(catalogEntryId) {
+ const payload = await requestJson<{
+ acknowledgment: ApiAuditCatalogChecklistItemAcknowledgmentView;
+ }>(
+ `/audit/catalog/${catalogEntryId}/acknowledgment`,
+ { method: "GET" },
+ "web.api.getAuditCatalogChecklistItemAcknowledgment",
+ );
+
+ return payload.acknowledgment;
+ },
+
async getAuditCatalogChecklistItemAttestation(catalogEntryId) {
const payload = await requestJson<{
attestation: ApiAuditCatalogChecklistItemAttestationView;
@@ -1369,6 +1435,18 @@ export function createRunrootApiClient(
return payload.attested;
},
+ async listAcknowledgedAuditCatalogEntries() {
+ const payload = await requestJson<{
+ acknowledged: ApiAuditCatalogChecklistItemAcknowledgmentCollection;
+ }>(
+ "/audit/catalog/acknowledged",
+ { method: "GET" },
+ "web.api.listAcknowledgedAuditCatalogEntries",
+ );
+
+ return payload.acknowledged;
+ },
+
async listVerifiedAuditCatalogEntries() {
const payload = await requestJson<{
verified: ApiAuditCatalogChecklistItemVerificationCollection;
@@ -1579,6 +1657,24 @@ export function createRunrootApiClient(
return payload.attestation;
},
+ async setAuditCatalogChecklistItemAcknowledgment(catalogEntryId, input) {
+ const payload = await requestJson<{
+ acknowledgment: ApiAuditCatalogChecklistItemAcknowledgmentView;
+ }>(
+ `/audit/catalog/${catalogEntryId}/acknowledgment`,
+ {
+ body: JSON.stringify(input),
+ headers: {
+ "content-type": "application/json",
+ },
+ method: "POST",
+ },
+ "web.api.setAuditCatalogChecklistItemAcknowledgment",
+ );
+
+ return payload.acknowledgment;
+ },
+
async setAuditCatalogAssignmentChecklist(catalogEntryId, input) {
const payload = await requestJson<{
checklist: ApiAuditCatalogAssignmentChecklistView;
diff --git a/docs/architecture/adr-0027-cross-run-audit-catalog-checklist-item-acknowledgments-and-acknowledgment-notes.md b/docs/architecture/adr-0027-cross-run-audit-catalog-checklist-item-acknowledgments-and-acknowledgment-notes.md
new file mode 100644
index 0000000..95378ac
--- /dev/null
+++ b/docs/architecture/adr-0027-cross-run-audit-catalog-checklist-item-acknowledgments-and-acknowledgment-notes.md
@@ -0,0 +1,108 @@
+# ADR-0027: Cross-Run Audit Catalog Checklist Item Acknowledgments and Acknowledgment Notes
+
+Date: 2026-04-03
+
+## Status
+
+Accepted on branch for Phase 27 implementation.
+
+## Context
+
+Phase 26 added a thin checklist-item-attestation layer over evidenced verified
+resolved blocked progressed assigned reviewed audit catalog entries. Operators
+could record whether stable evidence references had been attested, but there
+was still no package-owned way to capture that a downstream operator had
+acknowledged the current attestation and supporting evidence set without
+expanding into approval gating, workflow orchestration, artifact-vault
+behavior, attachment-upload products, provider-payload persistence, copied
+artifact persistence, or a broader collaboration system.
+
+The next repository-owned gap is still narrower than threaded collaboration,
+broader checklist orchestration, broader review workflow engines,
+fine-grained RBAC, multi-tenant access, dashboards, search, analytics,
+approval products, or attachment-upload products. Runroot needs a stable
+checklist-item-acknowledgment layer that remains derived over the existing
+checklist-item-attestation, checklist-item-evidence,
+checklist-item-verification, checklist-item-resolution,
+checklist-item-blocker, checklist-item-progress, assignment-checklist,
+review-assignment, review-signal, visibility, catalog, and saved-view seams.
+
+## Decision
+
+Runroot adds a shared audit-catalog checklist-item-acknowledgment contract
+with the following properties:
+
+- checklist item acknowledgments remain additive operator metadata
+- checklist item acknowledgments reference existing catalog entries,
+ checklist-item-attestations, checklist-item-evidence,
+ checklist-item-verifications, checklist-item-resolutions,
+ checklist-item-blockers, checklist-item-progress, assignment checklists,
+ review assignments, and review signals
+- checklist item acknowledgments store only stable per-item acknowledgment
+ state, a thin optional acknowledgment note, minimal actor and scope
+ references, and existing catalog refs
+- checklist item acknowledgments do not snapshot audit facts, provider
+ payloads, copied artifacts, or workflow state
+- checklist item acknowledgments do not change replay or approval source of
+ truth
+- applying an acknowledged preset reuses the existing catalog-apply path
+
+The shared acknowledgment seam is exposed through:
+
+- replay query helpers in `@runroot/replay`
+- persistence adapters in `@runroot/persistence`
+- operator methods in `@runroot/sdk`
+- thin HTTP routes in `apps/api`
+- thin command routing in `@runroot/cli`
+- minimal runs-page presentation in `apps/web`
+
+Acknowledgment visibility stays minimal:
+
+- list and inspect are scoped to operators already involved in the attestation,
+ evidence, verification, resolution, blocker, progress, and assignment
+ handoff path
+- acknowledgment entries can only reference checklist items that already exist
+ in the shared checklist-item-attestation layer
+- acknowledgment state stays as a thin per-item enum
+- acknowledgment notes stay as a single thin string
+- no threaded comments, checklist workflow engine, approval-gating product,
+ permission framework, organization directory, multi-tenant surface,
+ attachment-upload product, or artifact vault is added
+
+## Consequences
+
+### Positive
+
+- attested evidenced verified resolved blocked progressed assigned reviewed
+ presets can carry thin per-item acknowledgment state and a single
+ acknowledgment note through shared package-owned seams
+- inline and queued runs reuse the same checklist-item-acknowledgment contract
+- operator surfaces stay thin and reuse the existing catalog apply behavior
+- replay and approval semantics remain unchanged
+
+### Negative
+
+- checklist item acknowledgments are still a derived layer that depends on
+ checklist-item attestation, checklist-item evidence, checklist-item
+ verifications, checklist-item resolutions, checklist-item blockers,
+ checklist-item progress, assignment checklists, assignments, review signals,
+ visibility, and catalog integrity
+- acknowledgments intentionally stay shallow and do not solve approval
+ products, workflow gating, broader checklist orchestration, threaded
+ collaboration, payload persistence, binary artifact persistence, RBAC, or
+ multi-tenant requirements
+
+## Non-Goals
+
+This ADR does not introduce:
+
+- threaded comments
+- broader review workflow engines
+- broader checklist orchestration
+- acknowledgment-driven approval gating
+- attachment-upload or artifact-vault products
+- fine-grained RBAC
+- multi-tenant access control
+- dashboard, search, or analytics products
+- broader collaboration beyond thin operator-facing acknowledgment metadata
+- provider payload, copied artifact, or full snapshot persistence
diff --git a/docs/guides/audit-catalog-checklist-item-acknowledgments.md b/docs/guides/audit-catalog-checklist-item-acknowledgments.md
new file mode 100644
index 0000000..8ef7ef1
--- /dev/null
+++ b/docs/guides/audit-catalog-checklist-item-acknowledgments.md
@@ -0,0 +1,132 @@
+# Audit Catalog Checklist Item Acknowledgments
+
+Phase 27 adds a thin shared checklist-item-acknowledgment layer over attested
+evidenced verified resolved blocked progressed assigned reviewed audit catalog
+entries that already carry checklist-item-attestation metadata.
+
+## What Checklist Item Acknowledgments Record
+
+The shared contract stores:
+
+- a reference to an existing attested evidenced verified resolved blocked
+ progressed assigned reviewed catalog entry
+- per-item acknowledgment state for checklist items that already exist in the
+ shared checklist-item-attestation layer
+- minimal operator and scope references used by the current operator seam
+- an optional thin acknowledgment note
+
+The contract does not store:
+
+- provider-specific payloads
+- copied artifacts
+- workflow-state snapshots
+- replay or approval state
+- threaded comments, broader review workflow engines, or broader checklist
+ orchestration
+- fine-grained RBAC or multi-tenant access rules
+- surface-specific route formats
+- approval-gating product state
+- attachment-upload or artifact-vault product state
+
+Checklist item acknowledgments remain derived operator state. They do not
+replace replay, approval, saved views, catalog entries, visibility, review
+signals, review assignments, assignment checklists, checklist-item progress,
+checklist-item blockers, checklist-item resolutions, checklist-item
+verifications, checklist-item evidence, or checklist-item attestations.
+
+## Acknowledge, List-Acknowledged, Inspect-Acknowledgment, Clear-Acknowledgment, And Apply
+
+The minimum acknowledgment path is available through the existing seams:
+
+- SDK:
+ - `acknowledgeCatalogEntry(id, ...)`
+ - `listAcknowledgedCatalogEntries()`
+ - `getCatalogChecklistItemAcknowledgment(id)`
+ - `clearCatalogChecklistItemAcknowledgment(id)`
+ - `applyCatalogEntry(id)`
+- API:
+ - `POST /audit/catalog/:catalogEntryId/acknowledgment`
+ - `GET /audit/catalog/acknowledged`
+ - `GET /audit/catalog/:catalogEntryId/acknowledgment`
+ - `POST /audit/catalog/:catalogEntryId/acknowledgment/clear`
+ - `GET /audit/catalog/:catalogEntryId/apply`
+- CLI:
+ - `audit catalog acknowledge`
+ - `audit catalog acknowledged`
+ - `audit catalog inspect-acknowledgment`
+ - `audit catalog clear-acknowledgment`
+ - `audit catalog apply`
+- Web:
+ - the runs page presents a thin checklist-item-acknowledgment panel and a
+ minimal acknowledgment-note form over the existing catalog, visibility,
+ review-signal, review-assignment, assignment-checklist,
+ checklist-item-progress, checklist-item-blocker,
+ checklist-item-resolution, checklist-item-verification,
+ checklist-item-evidence, and checklist-item-attestation surfaces
+
+## What Applying An Acknowledged Preset Does
+
+Applying an acknowledged preset does not replay a run, reconstruct workflow
+state, or change approval semantics.
+
+It only:
+
+- resolves the visible catalog entry for the current operator identity
+- resolves additive review, assignment, checklist, progress, blocker,
+ resolution, verification, evidence, attestation, and acknowledgment
+ metadata for that entry
+- resolves the referenced saved view and constrained navigation metadata
+- reuses the existing catalog-apply and audit-navigation seams
+- returns the current navigation state for that acknowledged attested preset
+
+Replay and approval semantics still come only from persisted runtime and
+approval events.
+
+## Local Development
+
+Set a minimal operator identity for the current process:
+
+```bash
+$env:RUNROOT_OPERATOR_ID="ops_oncall"
+$env:RUNROOT_OPERATOR_SCOPE="ops"
+```
+
+Create and record acknowledgments on an attested evidenced verified resolved
+blocked progressed assigned reviewed shared preset:
+
+```bash
+pnpm dev:queued
+pnpm --filter @runroot/cli dev audit saved-views save --name "queued worker" --execution-mode queued --worker-id worker_1
+pnpm --filter @runroot/cli dev audit catalog publish saved_view_1 --name "Queued preset"
+pnpm --filter @runroot/cli dev audit catalog share catalog_entry_1
+pnpm --filter @runroot/cli dev audit catalog review catalog_entry_1 --state recommended --note "Ready for acknowledgment"
+pnpm --filter @runroot/cli dev audit catalog assign catalog_entry_1 --assignee ops_backup --handoff-note "Take the overnight follow-up"
+pnpm --filter @runroot/cli dev audit catalog checklist catalog_entry_1 --status pending --items-json "[\"Validate worker state\",\"Confirm saved drilldown\"]"
+pnpm --filter @runroot/cli dev audit catalog progress catalog_entry_1 --items-json "[{\"item\":\"Validate worker state\",\"state\":\"completed\"},{\"item\":\"Confirm saved drilldown\",\"state\":\"pending\"}]"
+pnpm --filter @runroot/cli dev audit catalog block catalog_entry_1 --items-json "[{\"item\":\"Validate worker state\",\"state\":\"cleared\"},{\"item\":\"Confirm saved drilldown\",\"state\":\"blocked\"}]" --blocker-note "Waiting for overnight handoff"
+pnpm --filter @runroot/cli dev audit catalog resolve catalog_entry_1 --items-json "[{\"item\":\"Validate worker state\",\"state\":\"resolved\"},{\"item\":\"Confirm saved drilldown\",\"state\":\"unresolved\"}]" --resolution-note "Backup confirmed the closeout"
+pnpm --filter @runroot/cli dev audit catalog verify catalog_entry_1 --items-json "[{\"item\":\"Validate worker state\",\"state\":\"verified\"},{\"item\":\"Confirm saved drilldown\",\"state\":\"unverified\"}]" --verification-note "Owner verified the closeout"
+pnpm --filter @runroot/cli dev audit catalog record-evidence catalog_entry_1 --items-json "[{\"item\":\"Validate worker state\",\"references\":[\"run://queued-worker/step/7\",\"note://backup-closeout\"]},{\"item\":\"Confirm saved drilldown\",\"references\":[\"doc://saved-drilldown\"]}]" --evidence-note "Thin evidence references only"
+pnpm --filter @runroot/cli dev audit catalog attest catalog_entry_1 --items-json "[{\"item\":\"Validate worker state\",\"state\":\"attested\"},{\"item\":\"Confirm saved drilldown\",\"state\":\"unattested\"}]" --attestation-note "Owner attested the stable evidence references"
+pnpm --filter @runroot/cli dev audit catalog acknowledge catalog_entry_1 --items-json "[{\"item\":\"Validate worker state\",\"state\":\"acknowledged\"},{\"item\":\"Confirm saved drilldown\",\"state\":\"unacknowledged\"}]" --acknowledgment-note "Backup acknowledged the attested evidence set"
+pnpm --filter @runroot/cli dev audit catalog acknowledged
+pnpm --filter @runroot/cli dev audit catalog apply catalog_entry_1
+pnpm --filter @runroot/cli dev audit catalog clear-acknowledgment catalog_entry_1
+```
+
+Both inline-originated and queued-originated presets reuse the same
+checklist-item-acknowledgment contract through the configured persistence
+adapter.
+
+## What Stays Deferred
+
+Still out of scope after Phase 27:
+
+- productized dashboards, discovery products, or broad analytics UX
+- open-ended search products
+- fine-grained RBAC, org or team management, and multi-tenant access models
+- threaded comments, broader checklist orchestration, broader review
+ workflows, or broader multi-user curation
+- full observability backend integrations
+- provider payload persistence, copied artifact persistence, approval-gating
+ products, attachment-upload, or artifact-vault products
diff --git a/docs/roadmap.md b/docs/roadmap.md
index ee1f24d..3f01718 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -427,4 +427,4 @@ Status: completed
engines, broader multi-user curation, approval products, or broad
observability and analytics platform work
-Status: scope frozen
+Status: completed
diff --git a/packages/cli/src/index.integration.test.ts b/packages/cli/src/index.integration.test.ts
index 946e666..a221364 100644
--- a/packages/cli/src/index.integration.test.ts
+++ b/packages/cli/src/index.integration.test.ts
@@ -4988,4 +4988,689 @@ describe("@runroot/cli integration", () => {
).toBe("Backup attested the stable follow-up evidence");
expect(attestedAfterClearPayload.attested.totalCount).toBe(0);
});
+
+ it("records acknowledgments, lists-acknowledged, inspects, clears, and reapplies attested catalog entries through the CLI", async () => {
+ const workspaceRoot = await mkdtemp(
+ join(tmpdir(), "runroot-cli-checklist-acknowledgment-"),
+ );
+ const sqlitePath = join(workspaceRoot, "runroot.sqlite");
+ const inputFile = join(workspaceRoot, "shell-runbook.json");
+ await writeFile(
+ inputFile,
+ JSON.stringify({
+ approvalRequired: false,
+ commandAlias: "print-ready",
+ runbookId: "node-health-check",
+ }),
+ );
+ const inlineStartIo = createIo();
+ const queuedStartIo = createIo();
+ const saveIo = createIo();
+ const publishIo = createIo();
+ const shareIo = createIo();
+ const reviewIo = createIo();
+ const assignIo = createIo();
+ const checklistIo = createIo();
+ const progressIo = createIo();
+ const blockerIo = createIo();
+ const resolutionIo = createIo();
+ const verificationIo = createIo();
+ const evidenceIo = createIo();
+ const attestationIo = createIo();
+ const acknowledgmentIo = createIo();
+ const acknowledgedPeerIo = createIo();
+ const inspectAcknowledgmentIo = createIo();
+ const applyIo = createIo();
+ const clearAcknowledgmentIo = createIo();
+ const acknowledgedAfterClearIo = createIo();
+
+ await runCli(
+ ["runs", "start", "shell-runbook-flow", "--input-file", inputFile],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: inlineStartIo.io,
+ },
+ );
+ const inlineRun = JSON.parse(inlineStartIo.stdout.join("")) as {
+ run: {
+ id: string;
+ };
+ };
+
+ await runCli(
+ ["runs", "start", "shell-runbook-flow", "--input-file", inputFile],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_EXECUTION_MODE: "queued",
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: queuedStartIo.io,
+ },
+ );
+ const queuedRun = JSON.parse(queuedStartIo.stdout.join("")) as {
+ run: {
+ id: string;
+ };
+ };
+
+ const worker = createRunrootWorkerService({
+ persistenceDriver: "sqlite",
+ sqlitePath,
+ workerId: "worker_cli_acknowledgment",
+ });
+ await worker.processNextJob();
+
+ await runCli(
+ [
+ "audit",
+ "saved-views",
+ "save",
+ "--name",
+ "Queued acknowledgment preset",
+ "--description",
+ "Saved queued worker acknowledgment preset",
+ "--execution-mode",
+ "queued",
+ "--worker-id",
+ "worker_cli_acknowledgment",
+ "--audit-view-run-id",
+ queuedRun.run.id,
+ "--drilldown-run-id",
+ queuedRun.run.id,
+ ],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: saveIo.io,
+ },
+ );
+ const savedViewPayload = JSON.parse(saveIo.stdout.join("")) as {
+ savedView: {
+ id: string;
+ };
+ };
+
+ const publishExitCode = await runCli(
+ ["audit", "catalog", "publish", savedViewPayload.savedView.id],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: publishIo.io,
+ },
+ );
+ const publishedPayload = JSON.parse(publishIo.stdout.join("")) as {
+ catalogEntry: {
+ entry: {
+ id: string;
+ };
+ };
+ };
+ const shareExitCode = await runCli(
+ ["audit", "catalog", "share", publishedPayload.catalogEntry.entry.id],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: shareIo.io,
+ },
+ );
+ const reviewExitCode = await runCli(
+ [
+ "audit",
+ "catalog",
+ "review",
+ publishedPayload.catalogEntry.entry.id,
+ "--state",
+ "recommended",
+ "--note",
+ `Acknowledgment ready after inline ${inlineRun.run.id}`,
+ ],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: reviewIo.io,
+ },
+ );
+ const assignExitCode = await runCli(
+ [
+ "audit",
+ "catalog",
+ "assign",
+ publishedPayload.catalogEntry.entry.id,
+ "--assignee",
+ "ops_backup",
+ "--handoff-note",
+ `Queued worker ${queuedRun.run.id} handed to backup`,
+ ],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: assignIo.io,
+ },
+ );
+ const checklistExitCode = await runCli(
+ [
+ "audit",
+ "catalog",
+ "checklist",
+ publishedPayload.catalogEntry.entry.id,
+ "--status",
+ "pending",
+ "--items-json",
+ JSON.stringify(["Validate queued follow-up", "Close backup handoff"]),
+ ],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: checklistIo.io,
+ },
+ );
+ const progressExitCode = await runCli(
+ [
+ "audit",
+ "catalog",
+ "progress",
+ publishedPayload.catalogEntry.entry.id,
+ "--items-json",
+ JSON.stringify([
+ {
+ item: "Validate queued follow-up",
+ state: "completed",
+ },
+ {
+ item: "Close backup handoff",
+ state: "pending",
+ },
+ ]),
+ "--completion-note",
+ "Queued follow-up is almost complete",
+ ],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: progressIo.io,
+ },
+ );
+ const blockerExitCode = await runCli(
+ [
+ "audit",
+ "catalog",
+ "block",
+ publishedPayload.catalogEntry.entry.id,
+ "--items-json",
+ JSON.stringify([
+ {
+ item: "Validate queued follow-up",
+ state: "cleared",
+ },
+ {
+ item: "Close backup handoff",
+ state: "blocked",
+ },
+ ]),
+ "--blocker-note",
+ "Waiting for the overnight handoff",
+ ],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: blockerIo.io,
+ },
+ );
+ const resolutionExitCode = await runCli(
+ [
+ "audit",
+ "catalog",
+ "resolve",
+ publishedPayload.catalogEntry.entry.id,
+ "--items-json",
+ JSON.stringify([
+ {
+ item: "Validate queued follow-up",
+ state: "resolved",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unresolved",
+ },
+ ]),
+ "--resolution-note",
+ "Backup confirmed the follow-up closure",
+ ],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: resolutionIo.io,
+ },
+ );
+ const verificationExitCode = await runCli(
+ [
+ "audit",
+ "catalog",
+ "verify",
+ publishedPayload.catalogEntry.entry.id,
+ "--items-json",
+ JSON.stringify([
+ {
+ item: "Validate queued follow-up",
+ state: "verified",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unverified",
+ },
+ ]),
+ "--verification-note",
+ "Backup verified the follow-up closure",
+ ],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: verificationIo.io,
+ },
+ );
+ const evidenceExitCode = await runCli(
+ [
+ "audit",
+ "catalog",
+ "record-evidence",
+ publishedPayload.catalogEntry.entry.id,
+ "--items-json",
+ JSON.stringify([
+ {
+ item: "Validate queued follow-up",
+ references: ["run://queued-follow-up", "note://backup-closeout"],
+ },
+ {
+ item: "Close backup handoff",
+ references: ["doc://backup-handoff"],
+ },
+ ]),
+ "--evidence-note",
+ "Backup collected stable follow-up references",
+ ],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: evidenceIo.io,
+ },
+ );
+ const attestationExitCode = await runCli(
+ [
+ "audit",
+ "catalog",
+ "attest",
+ publishedPayload.catalogEntry.entry.id,
+ "--items-json",
+ JSON.stringify([
+ {
+ item: "Validate queued follow-up",
+ state: "attested",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unattested",
+ },
+ ]),
+ "--attestation-note",
+ "Backup attested the stable follow-up evidence",
+ ],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: attestationIo.io,
+ },
+ );
+ const acknowledgmentExitCode = await runCli(
+ [
+ "audit",
+ "catalog",
+ "acknowledge",
+ publishedPayload.catalogEntry.entry.id,
+ "--items-json",
+ JSON.stringify([
+ {
+ item: "Validate queued follow-up",
+ state: "acknowledged",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unacknowledged",
+ },
+ ]),
+ "--acknowledgment-note",
+ "Backup acknowledged the attested follow-up",
+ ],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: acknowledgmentIo.io,
+ },
+ );
+ const acknowledgedPeerExitCode = await runCli(
+ ["audit", "catalog", "acknowledged"],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_backup",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: acknowledgedPeerIo.io,
+ },
+ );
+ const inspectAcknowledgmentExitCode = await runCli(
+ [
+ "audit",
+ "catalog",
+ "inspect-acknowledgment",
+ publishedPayload.catalogEntry.entry.id,
+ ],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: inspectAcknowledgmentIo.io,
+ },
+ );
+ const applyExitCode = await runCli(
+ ["audit", "catalog", "apply", publishedPayload.catalogEntry.entry.id],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_backup",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: applyIo.io,
+ },
+ );
+ const clearAcknowledgmentExitCode = await runCli(
+ [
+ "audit",
+ "catalog",
+ "clear-acknowledgment",
+ publishedPayload.catalogEntry.entry.id,
+ ],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_oncall",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: clearAcknowledgmentIo.io,
+ },
+ );
+ const acknowledgedAfterClearExitCode = await runCli(
+ ["audit", "catalog", "acknowledged"],
+ {
+ cwd: workspaceRoot,
+ env: {
+ RUNROOT_OPERATOR_ID: "ops_backup",
+ RUNROOT_OPERATOR_SCOPE: "ops",
+ RUNROOT_PERSISTENCE_DRIVER: "sqlite",
+ RUNROOT_SQLITE_PATH: sqlitePath,
+ },
+ io: acknowledgedAfterClearIo.io,
+ },
+ );
+ const acknowledgmentPayload = JSON.parse(
+ acknowledgmentIo.stdout.join(""),
+ ) as {
+ acknowledgment: {
+ acknowledgment: {
+ acknowledgmentNote?: string;
+ items: Array<{
+ item: string;
+ state: "acknowledged" | "unacknowledged";
+ }>;
+ };
+ attestation: {
+ attestation: {
+ attestationNote?: string;
+ };
+ evidence: {
+ evidence: {
+ evidenceNote?: string;
+ };
+ verification: {
+ verification: {
+ verificationNote?: string;
+ };
+ };
+ };
+ };
+ };
+ };
+ const acknowledgedPeerPayload = JSON.parse(
+ acknowledgedPeerIo.stdout.join(""),
+ ) as {
+ acknowledged: {
+ items: Array<{
+ acknowledgment: {
+ acknowledgmentNote?: string;
+ };
+ attestation: {
+ evidence: {
+ verification: {
+ resolution: {
+ blocker: {
+ progress: {
+ checklist: {
+ assignment: {
+ review: {
+ visibility: {
+ catalogEntry: {
+ entry: {
+ id: string;
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+ }>;
+ totalCount: number;
+ };
+ };
+ const inspectAcknowledgmentPayload = JSON.parse(
+ inspectAcknowledgmentIo.stdout.join(""),
+ ) as {
+ acknowledgment: {
+ acknowledgment: {
+ acknowledgmentNote?: string;
+ items: Array<{
+ item: string;
+ state: "acknowledged" | "unacknowledged";
+ }>;
+ };
+ };
+ };
+ const applyPayload = JSON.parse(applyIo.stdout.join("")) as {
+ application: {
+ application: {
+ navigation: {
+ drilldowns: Array<{
+ result: {
+ runId: string;
+ };
+ }>;
+ totalSummaryCount: number;
+ };
+ savedView: {
+ id: string;
+ };
+ };
+ };
+ };
+ const clearAcknowledgmentPayload = JSON.parse(
+ clearAcknowledgmentIo.stdout.join(""),
+ ) as {
+ acknowledgment: {
+ acknowledgment: {
+ acknowledgmentNote?: string;
+ };
+ };
+ };
+ const acknowledgedAfterClearPayload = JSON.parse(
+ acknowledgedAfterClearIo.stdout.join(""),
+ ) as {
+ acknowledged: {
+ totalCount: number;
+ };
+ };
+
+ expect(publishExitCode).toBe(0);
+ expect(shareExitCode).toBe(0);
+ expect(reviewExitCode).toBe(0);
+ expect(assignExitCode).toBe(0);
+ expect(checklistExitCode).toBe(0);
+ expect(progressExitCode).toBe(0);
+ expect(blockerExitCode).toBe(0);
+ expect(resolutionExitCode).toBe(0);
+ expect(verificationExitCode).toBe(0);
+ expect(evidenceExitCode).toBe(0);
+ expect(attestationExitCode).toBe(0);
+ expect(acknowledgmentExitCode).toBe(0);
+ expect(acknowledgedPeerExitCode).toBe(0);
+ expect(inspectAcknowledgmentExitCode).toBe(0);
+ expect(applyExitCode).toBe(0);
+ expect(clearAcknowledgmentExitCode).toBe(0);
+ expect(acknowledgedAfterClearExitCode).toBe(0);
+ expect(
+ acknowledgmentPayload.acknowledgment.attestation.evidence.verification
+ .verification.verificationNote,
+ ).toBe("Backup verified the follow-up closure");
+ expect(
+ acknowledgmentPayload.acknowledgment.attestation.evidence.evidence
+ .evidenceNote,
+ ).toBe("Backup collected stable follow-up references");
+ expect(
+ acknowledgmentPayload.acknowledgment.attestation.attestation
+ .attestationNote,
+ ).toBe("Backup attested the stable follow-up evidence");
+ expect(
+ acknowledgmentPayload.acknowledgment.acknowledgment.acknowledgmentNote,
+ ).toBe("Backup acknowledged the attested follow-up");
+ expect(acknowledgmentPayload.acknowledgment.acknowledgment.items).toEqual([
+ {
+ item: "Validate queued follow-up",
+ state: "acknowledged",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unacknowledged",
+ },
+ ]);
+ expect(acknowledgedPeerPayload.acknowledged.totalCount).toBe(1);
+ expect(
+ acknowledgedPeerPayload.acknowledged.items[0]?.attestation.evidence
+ .verification.resolution.blocker.progress.checklist.assignment.review
+ .visibility.catalogEntry.entry.id,
+ ).toBe(publishedPayload.catalogEntry.entry.id);
+ expect(
+ inspectAcknowledgmentPayload.acknowledgment.acknowledgment
+ .acknowledgmentNote,
+ ).toBe("Backup acknowledged the attested follow-up");
+ expect(applyPayload.application.application.savedView.id).toBe(
+ savedViewPayload.savedView.id,
+ );
+ expect(
+ applyPayload.application.application.navigation.drilldowns[0]?.result
+ .runId,
+ ).toBe(queuedRun.run.id);
+ expect(
+ clearAcknowledgmentPayload.acknowledgment.acknowledgment
+ .acknowledgmentNote,
+ ).toBe("Backup acknowledged the attested follow-up");
+ expect(acknowledgedAfterClearPayload.acknowledged.totalCount).toBe(0);
+ });
});
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index ca03198..19e24f6 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -5,6 +5,7 @@ import { readFile } from "node:fs/promises";
import type { PackageBoundary } from "@runroot/config";
import type { JsonValue, RunStatus } from "@runroot/domain";
import {
+ type AcknowledgeAuditCatalogEntryInput,
type AttestAuditCatalogEntryInput,
type BlockAuditCatalogEntryInput,
type ChecklistAuditCatalogEntryInput,
@@ -190,6 +191,19 @@ export async function runCli(
resolveAttestAuditCatalogEntryInput(flags),
),
});
+ case "acknowledge":
+ if (!detail) {
+ throw new Error(
+ "audit catalog acknowledge requires a catalog entry id.",
+ );
+ }
+
+ return writeJson(io.stdout.write, {
+ acknowledgment: await service.acknowledgeCatalogEntry(
+ detail,
+ resolveAcknowledgeAuditCatalogEntryInput(flags),
+ ),
+ });
case "record-evidence":
if (!detail) {
throw new Error(
@@ -227,6 +241,10 @@ export async function runCli(
return writeJson(io.stdout.write, {
attested: await service.listAttestedCatalogEntries(),
});
+ case "acknowledged":
+ return writeJson(io.stdout.write, {
+ acknowledged: await service.listAcknowledgedCatalogEntries(),
+ });
case "checklist":
if (!detail) {
throw new Error(
@@ -307,6 +325,17 @@ export async function runCli(
attestation:
await service.clearCatalogChecklistItemAttestation(detail),
});
+ case "clear-acknowledgment":
+ if (!detail) {
+ throw new Error(
+ "audit catalog clear-acknowledgment requires a catalog entry id.",
+ );
+ }
+
+ return writeJson(io.stdout.write, {
+ acknowledgment:
+ await service.clearCatalogChecklistItemAcknowledgment(detail),
+ });
case "clear-assignment":
if (!detail) {
throw new Error(
@@ -429,6 +458,17 @@ export async function runCli(
attestation:
await service.getCatalogChecklistItemAttestation(detail),
});
+ case "inspect-acknowledgment":
+ if (!detail) {
+ throw new Error(
+ "audit catalog inspect-acknowledgment requires a catalog entry id.",
+ );
+ }
+
+ return writeJson(io.stdout.write, {
+ acknowledgment:
+ await service.getCatalogChecklistItemAcknowledgment(detail),
+ });
case "inspect-review":
if (!detail) {
throw new Error(
@@ -931,6 +971,24 @@ function resolveAttestAuditCatalogEntryInput(
};
}
+function resolveAcknowledgeAuditCatalogEntryInput(
+ flags: ReadonlyMap,
+): AcknowledgeAuditCatalogEntryInput {
+ const itemsJson = getStringFlag(flags, "items-json");
+ const acknowledgmentNote = getStringFlag(flags, "acknowledgment-note");
+
+ if (!itemsJson) {
+ throw new Error(
+ "audit catalog acknowledge requires --items-json .",
+ );
+ }
+
+ return {
+ ...(acknowledgmentNote !== undefined ? { acknowledgmentNote } : {}),
+ items: resolveChecklistItemAcknowledgmentItems(itemsJson),
+ };
+}
+
function resolveRecordAuditCatalogEntryEvidenceInput(
flags: ReadonlyMap,
): RecordAuditCatalogEntryEvidenceInput {
@@ -1121,6 +1179,32 @@ function resolveChecklistItemAttestationItems(itemsJson: string): readonly {
return parsedItems;
}
+function resolveChecklistItemAcknowledgmentItems(itemsJson: string): readonly {
+ readonly item: string;
+ readonly state: "acknowledged" | "unacknowledged";
+}[] {
+ const parsedItems = JSON.parse(itemsJson) as unknown;
+
+ if (
+ !Array.isArray(parsedItems) ||
+ !parsedItems.every(
+ (item) =>
+ typeof item === "object" &&
+ item !== null &&
+ "item" in item &&
+ typeof item.item === "string" &&
+ "state" in item &&
+ (item.state === "acknowledged" || item.state === "unacknowledged"),
+ )
+ ) {
+ throw new Error(
+ "--items-json must decode to an array of { item, state } objects with state acknowledged|unacknowledged.",
+ );
+ }
+
+ return parsedItems;
+}
+
function getStringFlag(
flags: ReadonlyMap,
name: string,
@@ -1232,6 +1316,7 @@ Commands:
audit catalog verified
audit catalog evidenced
audit catalog attested
+ audit catalog acknowledged
audit catalog checklisted
audit catalog progressed
audit catalog publish [--name ] [--description ]
@@ -1243,6 +1328,7 @@ Commands:
audit catalog inspect-verification
audit catalog inspect-evidence
audit catalog inspect-attestation
+ audit catalog inspect-acknowledgment
audit catalog inspect-checklist
audit catalog inspect-progress
audit catalog inspect-review
@@ -1252,6 +1338,7 @@ Commands:
audit catalog verify --items-json [--verification-note ]
audit catalog record-evidence --items-json [--evidence-note ]
audit catalog attest --items-json [--attestation-note ]
+ audit catalog acknowledge --items-json [--acknowledgment-note ]
audit catalog checklist --status [--items-json ]
audit catalog progress --items-json [--completion-note ]
audit catalog clear-blocker
@@ -1259,6 +1346,7 @@ Commands:
audit catalog clear-verification
audit catalog clear-evidence
audit catalog clear-attestation
+ audit catalog clear-acknowledgment
audit catalog clear-assignment
audit catalog clear-checklist
audit catalog clear-progress
diff --git a/packages/config/src/index.test.ts b/packages/config/src/index.test.ts
index 3021974..e36ca39 100644
--- a/packages/config/src/index.test.ts
+++ b/packages/config/src/index.test.ts
@@ -11,9 +11,9 @@ import {
describe("@runroot/config", () => {
it("exposes phase-aware project metadata", () => {
expect(projectMetadata.name).toBe("Runroot");
- expect(projectMetadata.currentPhase).toBe(26);
+ expect(projectMetadata.currentPhase).toBe(27);
expect(projectMetadata.phaseName).toBe(
- "Cross-Run Audit Catalog Checklist Item Attestations and Attestation Notes",
+ "Cross-Run Audit Catalog Checklist Item Acknowledgments and Acknowledgment Notes",
);
});
diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts
index 6ea3341..06b7c24 100644
--- a/packages/config/src/index.ts
+++ b/packages/config/src/index.ts
@@ -26,7 +26,8 @@ export type DeliveryPhase =
| 23
| 24
| 25
- | 26;
+ | 26
+ | 27;
export type ExecutionMode = "inline" | "queued";
@@ -84,9 +85,9 @@ export const projectMetadata = {
name: "Runroot",
description:
"MCP-native runtime and orchestration for durable developer and ops workflows.",
- currentPhase: 26,
+ currentPhase: 27,
phaseName:
- "Cross-Run Audit Catalog Checklist Item Attestations and Attestation Notes",
+ "Cross-Run Audit Catalog Checklist Item Acknowledgments and Acknowledgment Notes",
} as const;
export const requiredQualityCommands = [
diff --git a/packages/persistence/src/catalog-checklist-item-acknowledgment-store.test.ts b/packages/persistence/src/catalog-checklist-item-acknowledgment-store.test.ts
new file mode 100644
index 0000000..c4964a9
--- /dev/null
+++ b/packages/persistence/src/catalog-checklist-item-acknowledgment-store.test.ts
@@ -0,0 +1,148 @@
+import { mkdtemp } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+
+import { createCrossRunAuditCatalogChecklistItemAcknowledgment } from "@runroot/replay";
+import { newDb } from "pg-mem";
+import { describe, expect, it } from "vitest";
+
+import {
+ createFileAuditCatalogChecklistItemAcknowledgmentStore,
+ createPostgresAuditCatalogChecklistItemAcknowledgmentStore,
+ createSqliteAuditCatalogChecklistItemAcknowledgmentStore,
+ resolveAuditCatalogChecklistItemAcknowledgmentFilePath,
+} from "./catalog-checklist-item-acknowledgment-store";
+
+function createAcknowledgment(
+ catalogEntryId: string,
+ timestamp = "2026-04-02T04:15:00.000Z",
+ acknowledgmentNote = "Operator acknowledged that the cited evidence is sufficient",
+) {
+ return createCrossRunAuditCatalogChecklistItemAcknowledgment({
+ acknowledgmentNote,
+ catalogEntryId,
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "acknowledged",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unacknowledged",
+ },
+ ],
+ operatorId: "ops_oncall",
+ scopeId: "ops",
+ timestamp,
+ });
+}
+
+describe("@runroot/persistence audit catalog checklist item acknowledgment stores", () => {
+ it("persists audit catalog checklist item acknowledgment through the Postgres adapter", async () => {
+ const memoryDatabase = newDb({
+ noAstCoverageCheck: true,
+ });
+ const { Pool } = memoryDatabase.adapters.createPg();
+ const pool = new Pool();
+ const firstStore =
+ createPostgresAuditCatalogChecklistItemAcknowledgmentStore({
+ pool,
+ });
+ const secondStore =
+ createPostgresAuditCatalogChecklistItemAcknowledgmentStore({
+ pool,
+ });
+
+ try {
+ await firstStore.saveCatalogChecklistItemAcknowledgment(
+ createAcknowledgment("catalog_entry_postgres"),
+ );
+
+ expect(
+ await secondStore.listCatalogChecklistItemAcknowledgments(),
+ ).toEqual([createAcknowledgment("catalog_entry_postgres")]);
+ } finally {
+ await pool.end();
+ }
+ });
+
+ it("persists audit catalog checklist item acknowledgment through the SQLite adapter", async () => {
+ const workspaceRoot = await mkdtemp(
+ join(tmpdir(), "runroot-acknowledgment-sqlite-"),
+ );
+ const filePath = join(workspaceRoot, "runroot.sqlite");
+ const firstStore = createSqliteAuditCatalogChecklistItemAcknowledgmentStore(
+ {
+ filePath,
+ },
+ );
+ const secondStore =
+ createSqliteAuditCatalogChecklistItemAcknowledgmentStore({
+ filePath,
+ });
+
+ await firstStore.saveCatalogChecklistItemAcknowledgment(
+ createAcknowledgment("catalog_entry_sqlite"),
+ );
+
+ expect(await secondStore.listCatalogChecklistItemAcknowledgments()).toEqual(
+ [createAcknowledgment("catalog_entry_sqlite")],
+ );
+ });
+
+ it("persists audit catalog checklist item acknowledgment through the file-sidecar compatibility path", async () => {
+ const workspaceRoot = await mkdtemp(
+ join(tmpdir(), "runroot-acknowledgment-file-"),
+ );
+ const workspacePath = join(workspaceRoot, "workspace.json");
+ const fileStore = createFileAuditCatalogChecklistItemAcknowledgmentStore({
+ filePath:
+ resolveAuditCatalogChecklistItemAcknowledgmentFilePath(workspacePath),
+ });
+
+ await fileStore.saveCatalogChecklistItemAcknowledgment(
+ createAcknowledgment("catalog_entry_file"),
+ );
+
+ expect(await fileStore.listCatalogChecklistItemAcknowledgments()).toEqual([
+ createAcknowledgment("catalog_entry_file"),
+ ]);
+ });
+
+ it("overwrites and clears audit catalog checklist item acknowledgment through the file-sidecar compatibility path", async () => {
+ const workspaceRoot = await mkdtemp(
+ join(tmpdir(), "runroot-acknowledgment-file-overwrite-"),
+ );
+ const workspacePath = join(workspaceRoot, "workspace.json");
+ const fileStore = createFileAuditCatalogChecklistItemAcknowledgmentStore({
+ filePath:
+ resolveAuditCatalogChecklistItemAcknowledgmentFilePath(workspacePath),
+ });
+ const originalEntry = createAcknowledgment("catalog_entry_overwrite");
+ const updatedEntry = createAcknowledgment(
+ "catalog_entry_overwrite",
+ "2026-04-02T04:15:10.000Z",
+ "Operator re-acknowledged the evidence after the queued handoff",
+ );
+
+ await fileStore.saveCatalogChecklistItemAcknowledgment(originalEntry);
+ await fileStore.saveCatalogChecklistItemAcknowledgment(updatedEntry);
+
+ expect(
+ await fileStore.getCatalogChecklistItemAcknowledgment(
+ "catalog_entry_overwrite",
+ ),
+ ).toEqual(updatedEntry);
+ expect(await fileStore.listCatalogChecklistItemAcknowledgments()).toEqual([
+ updatedEntry,
+ ]);
+ expect(
+ await fileStore.deleteCatalogChecklistItemAcknowledgment(
+ "catalog_entry_overwrite",
+ ),
+ ).toEqual(updatedEntry);
+ expect(await fileStore.listCatalogChecklistItemAcknowledgments()).toEqual(
+ [],
+ );
+ });
+});
diff --git a/packages/persistence/src/catalog-checklist-item-acknowledgment-store.ts b/packages/persistence/src/catalog-checklist-item-acknowledgment-store.ts
new file mode 100644
index 0000000..284a918
--- /dev/null
+++ b/packages/persistence/src/catalog-checklist-item-acknowledgment-store.ts
@@ -0,0 +1,870 @@
+import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
+import { dirname, join, parse, resolve } from "node:path";
+
+import {
+ type ResolvePersistenceConfigOptions,
+ resolvePersistenceConfig,
+} from "@runroot/config";
+import type {
+ CrossRunAuditCatalogChecklistItemAcknowledgment,
+ CrossRunAuditCatalogChecklistItemAcknowledgmentStore,
+} from "@runroot/replay";
+import { compareCrossRunAuditCatalogChecklistItemAcknowledgment } from "@runroot/replay";
+import type { Pool, PoolClient } from "pg";
+import type {
+ BindParams,
+ Database as SqliteDatabase,
+ SqlJsStatic,
+} from "sql.js";
+import initSqlJs from "sql.js/dist/sql-asm.js";
+
+import {
+ migratePostgresPersistence,
+ migrateSqlitePersistence,
+ type PostgresRuntimePersistenceOptions,
+ type SqliteRuntimePersistenceOptions,
+} from "./database-store";
+
+type SqlPrimitive = number | string | null;
+type SqlQueryRow = Readonly>;
+
+interface SqlClient {
+ readonly dialect: "postgres" | "sqlite";
+ execute(sql: string, params?: readonly SqlPrimitive[]): Promise;
+ queryRows(
+ sql: string,
+ params?: readonly SqlPrimitive[],
+ ): Promise;
+}
+
+type PostgresPoolLike = Pick;
+
+export interface InMemoryAuditCatalogChecklistItemAcknowledgmentStoreOptions {
+ readonly acknowledgmentEntries?: readonly CrossRunAuditCatalogChecklistItemAcknowledgment[];
+}
+
+export interface FileAuditCatalogChecklistItemAcknowledgmentStoreOptions {
+ readonly filePath: string;
+ readonly lockRetryDelayMs?: number;
+ readonly lockTimeoutMs?: number;
+}
+
+export interface PostgresAuditCatalogChecklistItemAcknowledgmentStoreOptions
+ extends Pick {}
+
+export interface SqliteAuditCatalogChecklistItemAcknowledgmentStoreOptions
+ extends Pick<
+ SqliteRuntimePersistenceOptions,
+ "filePath" | "lockRetryDelayMs" | "lockTimeoutMs"
+ > {}
+
+export interface ConfiguredAuditCatalogChecklistItemAcknowledgmentStoreOptions
+ extends ResolvePersistenceConfigOptions {
+ readonly filePath?: string;
+ readonly lockRetryDelayMs?: number;
+ readonly lockTimeoutMs?: number;
+ readonly pool?: PostgresPoolLike;
+}
+
+interface AuditCatalogChecklistItemAcknowledgmentSnapshot {
+ readonly acknowledgmentEntries: readonly CrossRunAuditCatalogChecklistItemAcknowledgment[];
+}
+
+let sqliteModulePromise: Promise | undefined;
+
+export function createConfiguredAuditCatalogChecklistItemAcknowledgmentStore(
+ options: ConfiguredAuditCatalogChecklistItemAcknowledgmentStoreOptions = {},
+): CrossRunAuditCatalogChecklistItemAcknowledgmentStore {
+ const resolved = resolvePersistenceConfig(options);
+
+ switch (resolved.driver) {
+ case "file":
+ return createFileAuditCatalogChecklistItemAcknowledgmentStore({
+ filePath:
+ options.filePath ??
+ resolveAuditCatalogChecklistItemAcknowledgmentFilePath(
+ resolved.workspacePath ?? resolved.location,
+ ),
+ ...(options.lockRetryDelayMs !== undefined
+ ? { lockRetryDelayMs: options.lockRetryDelayMs }
+ : {}),
+ ...(options.lockTimeoutMs !== undefined
+ ? { lockTimeoutMs: options.lockTimeoutMs }
+ : {}),
+ });
+ case "postgres":
+ return createPostgresAuditCatalogChecklistItemAcknowledgmentStore({
+ ...(resolved.databaseUrl ? { databaseUrl: resolved.databaseUrl } : {}),
+ ...(options.pool ? { pool: options.pool } : {}),
+ });
+ case "sqlite":
+ return createSqliteAuditCatalogChecklistItemAcknowledgmentStore({
+ filePath: resolved.sqlitePath ?? resolved.location,
+ ...(options.lockRetryDelayMs !== undefined
+ ? { lockRetryDelayMs: options.lockRetryDelayMs }
+ : {}),
+ ...(options.lockTimeoutMs !== undefined
+ ? { lockTimeoutMs: options.lockTimeoutMs }
+ : {}),
+ });
+ }
+}
+
+export function createInMemoryAuditCatalogChecklistItemAcknowledgmentStore(
+ options: InMemoryAuditCatalogChecklistItemAcknowledgmentStoreOptions = {},
+): CrossRunAuditCatalogChecklistItemAcknowledgmentStore {
+ const acknowledgmentEntries = [...(options.acknowledgmentEntries ?? [])].map(
+ (entry) => clone(entry),
+ );
+
+ return {
+ async deleteCatalogChecklistItemAcknowledgment(catalogEntryId) {
+ const existingIndex = acknowledgmentEntries.findIndex(
+ (entry) => entry.catalogEntryId === catalogEntryId,
+ );
+
+ if (existingIndex < 0) {
+ return undefined;
+ }
+
+ const [deletedEntry] = acknowledgmentEntries.splice(existingIndex, 1);
+
+ return deletedEntry ? clone(deletedEntry) : undefined;
+ },
+
+ async getCatalogChecklistItemAcknowledgment(catalogEntryId) {
+ const acknowledgment = acknowledgmentEntries.find(
+ (entry) => entry.catalogEntryId === catalogEntryId,
+ );
+
+ return acknowledgment ? clone(acknowledgment) : undefined;
+ },
+
+ async listCatalogChecklistItemAcknowledgments() {
+ return acknowledgmentEntries
+ .slice()
+ .sort(compareCrossRunAuditCatalogChecklistItemAcknowledgment)
+ .map((entry) => clone(entry));
+ },
+
+ async saveCatalogChecklistItemAcknowledgment(entry) {
+ const existingIndex = acknowledgmentEntries.findIndex(
+ (candidate) => candidate.catalogEntryId === entry.catalogEntryId,
+ );
+
+ if (existingIndex >= 0) {
+ acknowledgmentEntries.splice(existingIndex, 1);
+ }
+
+ acknowledgmentEntries.push(clone(entry));
+ acknowledgmentEntries.sort(
+ compareCrossRunAuditCatalogChecklistItemAcknowledgment,
+ );
+
+ return clone(entry);
+ },
+ };
+}
+
+export function createFileAuditCatalogChecklistItemAcknowledgmentStore(
+ options: FileAuditCatalogChecklistItemAcknowledgmentStoreOptions,
+): CrossRunAuditCatalogChecklistItemAcknowledgmentStore {
+ const filePath = resolve(options.filePath);
+ let accessQueue = Promise.resolve();
+
+ return {
+ async deleteCatalogChecklistItemAcknowledgment(catalogEntryId) {
+ return enqueueAccess(async () =>
+ withMutableSnapshot(filePath, options, async (snapshot) => {
+ const existingEntry = snapshot.acknowledgmentEntries.find(
+ (entry) => entry.catalogEntryId === catalogEntryId,
+ );
+
+ if (!existingEntry) {
+ return undefined;
+ }
+
+ await writeAuditCatalogChecklistItemAcknowledgmentSnapshot(filePath, {
+ acknowledgmentEntries: snapshot.acknowledgmentEntries.filter(
+ (entry) => entry.catalogEntryId !== catalogEntryId,
+ ),
+ });
+
+ return clone(existingEntry);
+ }),
+ );
+ },
+
+ async getCatalogChecklistItemAcknowledgment(catalogEntryId) {
+ return enqueueAccess(async () =>
+ withReadOnlySnapshot(filePath, (snapshot) => {
+ const acknowledgment = snapshot.acknowledgmentEntries.find(
+ (entry) => entry.catalogEntryId === catalogEntryId,
+ );
+
+ return acknowledgment ? clone(acknowledgment) : undefined;
+ }),
+ );
+ },
+
+ async listCatalogChecklistItemAcknowledgments() {
+ return enqueueAccess(async () =>
+ withReadOnlySnapshot(filePath, (snapshot) =>
+ snapshot.acknowledgmentEntries
+ .slice()
+ .sort(compareCrossRunAuditCatalogChecklistItemAcknowledgment)
+ .map((entry) => clone(entry)),
+ ),
+ );
+ },
+
+ async saveCatalogChecklistItemAcknowledgment(entry) {
+ return enqueueAccess(async () =>
+ withMutableSnapshot(filePath, options, async (snapshot) => {
+ const nextAcknowledgmentEntries = [
+ ...snapshot.acknowledgmentEntries.filter(
+ (candidate) => candidate.catalogEntryId !== entry.catalogEntryId,
+ ),
+ clone(entry),
+ ].sort(compareCrossRunAuditCatalogChecklistItemAcknowledgment);
+
+ await writeAuditCatalogChecklistItemAcknowledgmentSnapshot(filePath, {
+ acknowledgmentEntries: nextAcknowledgmentEntries,
+ });
+
+ return clone(entry);
+ }),
+ );
+ },
+ };
+
+ async function enqueueAccess(
+ accessOperation: () => Promise,
+ ): Promise {
+ const pendingAccess = accessQueue.then(accessOperation, accessOperation);
+ accessQueue = pendingAccess.then(
+ () => undefined,
+ () => undefined,
+ );
+
+ return pendingAccess;
+ }
+}
+
+export function createPostgresAuditCatalogChecklistItemAcknowledgmentStore(
+ options: PostgresAuditCatalogChecklistItemAcknowledgmentStoreOptions = {},
+): CrossRunAuditCatalogChecklistItemAcknowledgmentStore {
+ const pool = options.pool ?? createDefaultPool(options.databaseUrl);
+ let schemaReadyPromise: Promise | undefined;
+
+ return createDatabaseAuditCatalogChecklistItemAcknowledgmentStore({
+ ensureSchema() {
+ schemaReadyPromise ??= migratePostgresPersistence({
+ ...(options.databaseUrl ? { databaseUrl: options.databaseUrl } : {}),
+ ...(options.pool ? { pool: options.pool } : {}),
+ }).then(() => undefined);
+
+ return schemaReadyPromise;
+ },
+ withReadOnlyClient(task) {
+ return withPostgresClient(pool, task);
+ },
+ withTransaction(task) {
+ return withPostgresTransaction(pool, task);
+ },
+ });
+}
+
+export function createSqliteAuditCatalogChecklistItemAcknowledgmentStore(
+ options: SqliteAuditCatalogChecklistItemAcknowledgmentStoreOptions,
+): CrossRunAuditCatalogChecklistItemAcknowledgmentStore {
+ const filePath = resolve(options.filePath);
+ let accessQueue = Promise.resolve();
+ let schemaReadyPromise: Promise | undefined;
+
+ return createDatabaseAuditCatalogChecklistItemAcknowledgmentStore({
+ ensureSchema() {
+ schemaReadyPromise ??= migrateSqlitePersistence({
+ filePath,
+ }).then(() => undefined);
+
+ return schemaReadyPromise;
+ },
+ withReadOnlyClient(task) {
+ return enqueueAccess(async () =>
+ withSqliteClient(
+ {
+ filePath,
+ mutable: false,
+ },
+ task,
+ ),
+ );
+ },
+ withTransaction(task) {
+ return enqueueAccess(async () =>
+ withFileLock(filePath, options, async () =>
+ withSqliteClient(
+ {
+ filePath,
+ mutable: true,
+ },
+ task,
+ ),
+ ),
+ );
+ },
+ });
+
+ async function enqueueAccess(
+ action: () => Promise,
+ ): Promise {
+ const pendingAccess = accessQueue.then(action, action);
+ accessQueue = pendingAccess.then(
+ () => undefined,
+ () => undefined,
+ );
+
+ return pendingAccess;
+ }
+}
+
+function createDatabaseAuditCatalogChecklistItemAcknowledgmentStore(options: {
+ readonly ensureSchema: () => Promise;
+ readonly withReadOnlyClient: (
+ task: (client: SqlClient) => Promise,
+ ) => Promise;
+ readonly withTransaction: (
+ task: (client: SqlClient) => Promise,
+ ) => Promise;
+}): CrossRunAuditCatalogChecklistItemAcknowledgmentStore {
+ return {
+ async deleteCatalogChecklistItemAcknowledgment(catalogEntryId) {
+ await options.ensureSchema();
+
+ return options.withTransaction(async (client) => {
+ const existingRows = await client.queryRows<{ data: string }>(
+ `SELECT data
+ FROM runroot_audit_catalog_checklist_item_acknowledgment
+ WHERE catalog_entry_id = ?`,
+ [catalogEntryId],
+ );
+
+ if (!existingRows[0]) {
+ return undefined;
+ }
+
+ await client.execute(
+ `DELETE FROM runroot_audit_catalog_checklist_item_acknowledgment
+ WHERE catalog_entry_id = ?`,
+ [catalogEntryId],
+ );
+
+ return deserializeRow(
+ existingRows[0].data,
+ );
+ });
+ },
+
+ async getCatalogChecklistItemAcknowledgment(catalogEntryId) {
+ await options.ensureSchema();
+
+ return options.withReadOnlyClient(async (client) => {
+ const rows = await client.queryRows<{ data: string }>(
+ `SELECT data
+ FROM runroot_audit_catalog_checklist_item_acknowledgment
+ WHERE catalog_entry_id = ?`,
+ [catalogEntryId],
+ );
+
+ return rows[0]
+ ? deserializeRow(
+ rows[0].data,
+ )
+ : undefined;
+ });
+ },
+
+ async listCatalogChecklistItemAcknowledgments() {
+ await options.ensureSchema();
+
+ return options.withReadOnlyClient(async (client) => {
+ const rows = await client.queryRows<{ data: string }>(
+ `SELECT data
+ FROM runroot_audit_catalog_checklist_item_acknowledgment`,
+ );
+
+ return rows
+ .map((row) =>
+ deserializeRow(
+ row.data,
+ ),
+ )
+ .sort(compareCrossRunAuditCatalogChecklistItemAcknowledgment);
+ });
+ },
+
+ async saveCatalogChecklistItemAcknowledgment(entry) {
+ await options.ensureSchema();
+
+ return options.withTransaction(async (client) => {
+ await client.execute(
+ `INSERT INTO runroot_audit_catalog_checklist_item_acknowledgment (
+ catalog_entry_id,
+ kind,
+ operator_id,
+ scope_id,
+ acknowledgment_note,
+ acknowledgment_items,
+ created_at,
+ updated_at,
+ data
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ON CONFLICT (catalog_entry_id) DO UPDATE SET
+ kind = excluded.kind,
+ operator_id = excluded.operator_id,
+ scope_id = excluded.scope_id,
+ acknowledgment_note = excluded.acknowledgment_note,
+ acknowledgment_items = excluded.acknowledgment_items,
+ created_at = excluded.created_at,
+ updated_at = excluded.updated_at,
+ data = excluded.data`,
+ [
+ entry.catalogEntryId,
+ entry.kind,
+ entry.operatorId,
+ entry.scopeId,
+ entry.acknowledgmentNote ?? null,
+ JSON.stringify(entry.items),
+ entry.createdAt,
+ entry.updatedAt,
+ serializeRow(entry),
+ ],
+ );
+
+ return clone(entry);
+ });
+ },
+ };
+}
+
+export function resolveAuditCatalogChecklistItemAcknowledgmentFilePath(
+ workspacePath: string,
+): string {
+ const resolvedPath = resolve(workspacePath);
+ const parsedPath = parse(resolvedPath);
+
+ return join(
+ parsedPath.dir,
+ `${parsedPath.name}.audit-catalog-checklist-item-acknowledgment.json`,
+ );
+}
+
+async function withReadOnlySnapshot(
+ filePath: string,
+ action: (
+ snapshot: AuditCatalogChecklistItemAcknowledgmentSnapshot,
+ ) => TValue | Promise,
+): Promise {
+ const snapshot =
+ await readAuditCatalogChecklistItemAcknowledgmentSnapshot(filePath);
+
+ return action(snapshot);
+}
+
+async function withMutableSnapshot(
+ filePath: string,
+ options: Pick<
+ FileAuditCatalogChecklistItemAcknowledgmentStoreOptions,
+ "lockRetryDelayMs" | "lockTimeoutMs"
+ >,
+ action: (
+ snapshot: AuditCatalogChecklistItemAcknowledgmentSnapshot,
+ ) => Promise,
+): Promise {
+ await ensureParentDirectory(filePath);
+
+ return withFileLock(filePath, options, async () =>
+ action(await readAuditCatalogChecklistItemAcknowledgmentSnapshot(filePath)),
+ );
+}
+
+async function readAuditCatalogChecklistItemAcknowledgmentSnapshot(
+ filePath: string,
+): Promise {
+ try {
+ const rawSnapshot = await readFile(filePath, "utf8");
+ const parsedSnapshot = JSON.parse(
+ rawSnapshot,
+ ) as AuditCatalogChecklistItemAcknowledgmentSnapshot;
+
+ return {
+ acknowledgmentEntries: [
+ ...(parsedSnapshot.acknowledgmentEntries ?? []),
+ ].map((entry) => clone(entry)),
+ };
+ } catch (error) {
+ if (isMissingFileError(error)) {
+ return {
+ acknowledgmentEntries: [],
+ };
+ }
+
+ throw error;
+ }
+}
+
+async function writeAuditCatalogChecklistItemAcknowledgmentSnapshot(
+ filePath: string,
+ snapshot: AuditCatalogChecklistItemAcknowledgmentSnapshot,
+): Promise {
+ const tempPath = `${filePath}.tmp`;
+ const backupPath = `${filePath}.bak`;
+
+ await writeFile(tempPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8");
+
+ try {
+ await rename(tempPath, filePath);
+
+ return;
+ } catch (error) {
+ if (!isExistingFileError(error)) {
+ throw error;
+ }
+ }
+
+ await rm(backupPath, {
+ force: true,
+ });
+
+ try {
+ await rename(filePath, backupPath);
+ await rename(tempPath, filePath);
+ } catch (error) {
+ await rm(tempPath, {
+ force: true,
+ });
+
+ try {
+ await rename(backupPath, filePath);
+ } catch (restoreError) {
+ if (!isMissingFileError(restoreError)) {
+ throw restoreError;
+ }
+ }
+
+ throw error;
+ }
+
+ await rm(backupPath, {
+ force: true,
+ });
+}
+
+async function withPostgresClient(
+ pool: PostgresPoolLike,
+ task: (client: SqlClient) => Promise,
+): Promise {
+ const client = await pool.connect();
+
+ try {
+ return await task(new PostgresSqlClient(client));
+ } finally {
+ client.release();
+ }
+}
+
+async function withPostgresTransaction(
+ pool: PostgresPoolLike,
+ task: (client: SqlClient) => Promise,
+): Promise {
+ const client = await pool.connect();
+
+ try {
+ await client.query("BEGIN");
+ const result = await task(new PostgresSqlClient(client));
+ await client.query("COMMIT");
+
+ return result;
+ } catch (error) {
+ await client.query("ROLLBACK");
+
+ throw error;
+ } finally {
+ client.release();
+ }
+}
+
+async function withSqliteClient(
+ options: {
+ readonly filePath: string;
+ readonly mutable: boolean;
+ },
+ task: (client: SqlClient) => Promise,
+): Promise {
+ if (options.mutable) {
+ await ensureParentDirectory(options.filePath);
+ }
+
+ const SQL = await loadSqliteModule();
+ const database = await openSqliteDatabase(SQL, options.filePath);
+
+ try {
+ const client = new SqliteSqlClient(database);
+
+ if (!options.mutable) {
+ return task(client);
+ }
+
+ await client.execute("BEGIN");
+
+ try {
+ const result = await task(client);
+ await client.execute("COMMIT");
+ await writeBinaryFileAtomically(options.filePath, database.export());
+
+ return result;
+ } catch (error) {
+ await client.execute("ROLLBACK");
+
+ throw error;
+ }
+ } finally {
+ database.close();
+ }
+}
+
+async function openSqliteDatabase(
+ SQL: SqlJsStatic,
+ filePath: string,
+): Promise {
+ try {
+ const contents = await readFile(filePath);
+
+ return new SQL.Database(new Uint8Array(contents));
+ } catch (error) {
+ if (isMissingFileError(error)) {
+ return new SQL.Database();
+ }
+
+ throw error;
+ }
+}
+
+async function loadSqliteModule(): Promise {
+ sqliteModulePromise ??= initSqlJs();
+
+ return sqliteModulePromise;
+}
+
+class PostgresSqlClient implements SqlClient {
+ readonly dialect = "postgres" as const;
+
+ constructor(private readonly client: PoolClient) {}
+
+ async execute(
+ sql: string,
+ params: readonly SqlPrimitive[] = [],
+ ): Promise {
+ const result = await this.client.query(
+ convertQuestionMarksToPostgres(sql),
+ [...params],
+ );
+
+ return result.rowCount ?? 0;
+ }
+
+ async queryRows(
+ sql: string,
+ params: readonly SqlPrimitive[] = [],
+ ): Promise {
+ const result = await this.client.query(
+ convertQuestionMarksToPostgres(sql),
+ [...params],
+ );
+
+ return result.rows;
+ }
+}
+
+class SqliteSqlClient implements SqlClient {
+ readonly dialect = "sqlite" as const;
+
+ constructor(private readonly database: SqliteDatabase) {}
+
+ async execute(
+ sql: string,
+ params: readonly SqlPrimitive[] = [],
+ ): Promise {
+ this.database.run(sql, toSqliteParams(params));
+
+ return this.database.getRowsModified();
+ }
+
+ async queryRows(
+ sql: string,
+ params: readonly SqlPrimitive[] = [],
+ ): Promise {
+ const statement = this.database.prepare(sql, toSqliteParams(params));
+ const rows: TRow[] = [];
+
+ try {
+ while (statement.step()) {
+ rows.push(statement.getAsObject() as TRow);
+ }
+ } finally {
+ statement.free();
+ }
+
+ return rows;
+ }
+}
+
+function createDefaultPool(databaseUrl?: string): PostgresPoolLike {
+ if (!databaseUrl) {
+ throw new Error(
+ 'Postgres audit catalog checklist item acknowledgment requires DATABASE_URL or an explicit "databaseUrl" option.',
+ );
+ }
+
+ const { Pool } = require("pg") as typeof import("pg");
+
+ return new Pool({
+ connectionString: databaseUrl,
+ });
+}
+
+function toSqliteParams(params: readonly SqlPrimitive[]): BindParams {
+ return [...params];
+}
+
+function convertQuestionMarksToPostgres(sql: string): string {
+ let placeholderIndex = 0;
+
+ return sql.replace(/\?/g, () => `$${++placeholderIndex}`);
+}
+
+async function ensureParentDirectory(filePath: string): Promise {
+ await mkdir(dirname(filePath), {
+ recursive: true,
+ });
+}
+
+async function writeBinaryFileAtomically(
+ filePath: string,
+ contents: Uint8Array,
+): Promise {
+ const tempPath = `${filePath}.tmp`;
+ const backupPath = `${filePath}.bak`;
+
+ await writeFile(tempPath, Buffer.from(contents));
+
+ try {
+ await rename(tempPath, filePath);
+
+ return;
+ } catch (error) {
+ if (!isExistingFileError(error)) {
+ throw error;
+ }
+ }
+
+ await rm(backupPath, {
+ force: true,
+ });
+
+ try {
+ await rename(filePath, backupPath);
+ await rename(tempPath, filePath);
+ } catch (error) {
+ await rm(tempPath, {
+ force: true,
+ });
+
+ try {
+ await rename(backupPath, filePath);
+ } catch (restoreError) {
+ if (!isMissingFileError(restoreError)) {
+ throw restoreError;
+ }
+ }
+
+ throw error;
+ }
+
+ await rm(backupPath, {
+ force: true,
+ });
+}
+
+async function withFileLock(
+ filePath: string,
+ options: Pick<
+ | FileAuditCatalogChecklistItemAcknowledgmentStoreOptions
+ | SqliteAuditCatalogChecklistItemAcknowledgmentStoreOptions,
+ "lockRetryDelayMs" | "lockTimeoutMs"
+ >,
+ action: () => Promise,
+): Promise {
+ const lockPath = `${filePath}.lock`;
+ const retryDelayMs = options.lockRetryDelayMs ?? 25;
+ const timeoutMs = options.lockTimeoutMs ?? 5_000;
+ const startedAt = Date.now();
+
+ while (true) {
+ try {
+ const lockHandle = await open(lockPath, "wx");
+
+ try {
+ return await action();
+ } finally {
+ await lockHandle.close();
+ await rm(lockPath, {
+ force: true,
+ });
+ }
+ } catch (error) {
+ if (!isExistingFileError(error)) {
+ throw error;
+ }
+
+ if (Date.now() - startedAt >= timeoutMs) {
+ throw new Error(
+ `Timed out waiting for audit catalog checklist item acknowledgment lock at "${lockPath}".`,
+ );
+ }
+
+ await delay(retryDelayMs);
+ }
+ }
+}
+
+function delay(durationMs: number): Promise {
+ return new Promise((resolveDelay) => {
+ setTimeout(resolveDelay, durationMs);
+ });
+}
+
+function serializeRow(value: unknown): string {
+ return JSON.stringify(value);
+}
+
+function deserializeRow(rawValue: string): TValue {
+ return JSON.parse(rawValue) as TValue;
+}
+
+function clone(value: TValue): TValue {
+ return structuredClone(value);
+}
+
+function isExistingFileError(error: unknown): boolean {
+ return (
+ error instanceof Error &&
+ "code" in error &&
+ (error.code === "EEXIST" || error.code === "EPERM")
+ );
+}
+
+function isMissingFileError(error: unknown): boolean {
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
+}
diff --git a/packages/persistence/src/index.ts b/packages/persistence/src/index.ts
index 21a327a..d2eebd7 100644
--- a/packages/persistence/src/index.ts
+++ b/packages/persistence/src/index.ts
@@ -13,6 +13,19 @@ export {
resolveAuditCatalogAssignmentChecklistsFilePath,
type SqliteAuditCatalogAssignmentChecklistStoreOptions,
} from "./catalog-assignment-checklist-store";
+export {
+ type ConfiguredAuditCatalogChecklistItemAcknowledgmentStoreOptions,
+ createConfiguredAuditCatalogChecklistItemAcknowledgmentStore,
+ createFileAuditCatalogChecklistItemAcknowledgmentStore,
+ createInMemoryAuditCatalogChecklistItemAcknowledgmentStore,
+ createPostgresAuditCatalogChecklistItemAcknowledgmentStore,
+ createSqliteAuditCatalogChecklistItemAcknowledgmentStore,
+ type FileAuditCatalogChecklistItemAcknowledgmentStoreOptions,
+ type InMemoryAuditCatalogChecklistItemAcknowledgmentStoreOptions,
+ type PostgresAuditCatalogChecklistItemAcknowledgmentStoreOptions,
+ resolveAuditCatalogChecklistItemAcknowledgmentFilePath,
+ type SqliteAuditCatalogChecklistItemAcknowledgmentStoreOptions,
+} from "./catalog-checklist-item-acknowledgment-store";
export {
type ConfiguredAuditCatalogChecklistItemAttestationStoreOptions,
createConfiguredAuditCatalogChecklistItemAttestationStore,
@@ -212,7 +225,7 @@ export const persistencePackageBoundary = {
kind: "package",
phaseOwned: 2,
responsibility:
- "Repository contracts, checkpoint storage, database adapters, dispatch queue persistence seams, tool-history storage adapters, additive saved-audit-view adapters, additive audit-view-catalog adapters, additive catalog-visibility adapters, additive catalog-review-signal adapters, additive catalog-review-assignment adapters, additive catalog-assignment-checklist adapters, additive checklist-item-progress adapters, additive checklist-item-blocker adapters, additive checklist-item-resolution adapters, additive checklist-item-verification adapters, additive checklist-item-evidence adapters, and additive checklist-item-attestation adapters.",
+ "Repository contracts, checkpoint storage, database adapters, dispatch queue persistence seams, tool-history storage adapters, additive saved-audit-view adapters, additive audit-view-catalog adapters, additive catalog-visibility adapters, additive catalog-review-signal adapters, additive catalog-review-assignment adapters, additive catalog-assignment-checklist adapters, additive checklist-item-progress adapters, additive checklist-item-blocker adapters, additive checklist-item-resolution adapters, additive checklist-item-verification adapters, additive checklist-item-evidence adapters, additive checklist-item-attestation adapters, and additive checklist-item-acknowledgment adapters.",
publicSurface: [
"repository interfaces",
"storage adapters",
diff --git a/packages/persistence/src/migrations.ts b/packages/persistence/src/migrations.ts
index e85de6b..4213ad1 100644
--- a/packages/persistence/src/migrations.ts
+++ b/packages/persistence/src/migrations.ts
@@ -628,6 +628,43 @@ export const runtimePersistenceMigrations = [
ON runroot_audit_catalog_checklist_item_attestation (operator_id, updated_at DESC, catalog_entry_id ASC)`,
],
},
+ {
+ id: "0016_audit_catalog_checklist_item_acknowledgment",
+ postgres: [
+ `CREATE TABLE IF NOT EXISTS runroot_audit_catalog_checklist_item_acknowledgment (
+ catalog_entry_id TEXT PRIMARY KEY,
+ kind TEXT NOT NULL,
+ operator_id TEXT NOT NULL,
+ scope_id TEXT NOT NULL,
+ acknowledgment_note TEXT,
+ acknowledgment_items TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL,
+ data TEXT NOT NULL
+ )`,
+ `CREATE INDEX IF NOT EXISTS idx_runroot_audit_catalog_checklist_item_acknowledgment_scope
+ ON runroot_audit_catalog_checklist_item_acknowledgment (scope_id, updated_at DESC, catalog_entry_id ASC)`,
+ `CREATE INDEX IF NOT EXISTS idx_runroot_audit_catalog_checklist_item_acknowledgment_operator
+ ON runroot_audit_catalog_checklist_item_acknowledgment (operator_id, updated_at DESC, catalog_entry_id ASC)`,
+ ],
+ sqlite: [
+ `CREATE TABLE IF NOT EXISTS runroot_audit_catalog_checklist_item_acknowledgment (
+ catalog_entry_id TEXT PRIMARY KEY,
+ kind TEXT NOT NULL,
+ operator_id TEXT NOT NULL,
+ scope_id TEXT NOT NULL,
+ acknowledgment_note TEXT,
+ acknowledgment_items TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL,
+ data TEXT NOT NULL
+ )`,
+ `CREATE INDEX IF NOT EXISTS idx_runroot_audit_catalog_checklist_item_acknowledgment_scope
+ ON runroot_audit_catalog_checklist_item_acknowledgment (scope_id, updated_at DESC, catalog_entry_id ASC)`,
+ `CREATE INDEX IF NOT EXISTS idx_runroot_audit_catalog_checklist_item_acknowledgment_operator
+ ON runroot_audit_catalog_checklist_item_acknowledgment (operator_id, updated_at DESC, catalog_entry_id ASC)`,
+ ],
+ },
] as const satisfies readonly PersistenceMigration[];
export function getRuntimePersistenceMigrationStatements(
diff --git a/packages/replay/src/checklist-item-acknowledgment.test.ts b/packages/replay/src/checklist-item-acknowledgment.test.ts
new file mode 100644
index 0000000..0348045
--- /dev/null
+++ b/packages/replay/src/checklist-item-acknowledgment.test.ts
@@ -0,0 +1,99 @@
+import { describe, expect, it } from "vitest";
+
+import {
+ compareCrossRunAuditCatalogChecklistItemAcknowledgment,
+ createCrossRunAuditCatalogChecklistItemAcknowledgment,
+ normalizeChecklistItemAcknowledgmentItems,
+} from "./checklist-item-acknowledgment";
+
+describe("@runroot/replay audit catalog checklist item acknowledgment", () => {
+ it("creates checklist item acknowledgment with normalized metadata", () => {
+ const acknowledgment =
+ createCrossRunAuditCatalogChecklistItemAcknowledgment({
+ acknowledgmentNote: " Evidence is sufficient for the queued closeout ",
+ catalogEntryId: " catalog_entry_acknowledgment ",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "acknowledged",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unacknowledged",
+ },
+ ],
+ operatorId: " ops_oncall ",
+ scopeId: " ops ",
+ timestamp: "2026-04-02T04:00:00.000Z",
+ });
+
+ expect(acknowledgment).toEqual({
+ acknowledgmentNote: "Evidence is sufficient for the queued closeout",
+ catalogEntryId: "catalog_entry_acknowledgment",
+ createdAt: "2026-04-02T04:00:00.000Z",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "acknowledged",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unacknowledged",
+ },
+ ],
+ kind: "catalog-checklist-item-acknowledgment",
+ operatorId: "ops_oncall",
+ scopeId: "ops",
+ updatedAt: "2026-04-02T04:00:00.000Z",
+ });
+ });
+
+ it("restricts checklist item acknowledgment to shared evidence items", () => {
+ expect(() =>
+ normalizeChecklistItemAcknowledgmentItems(
+ [
+ {
+ item: "Missing item",
+ state: "acknowledged",
+ },
+ ],
+ ["Validate queued follow-up", "Close backup handoff"],
+ ),
+ ).toThrow(
+ 'Catalog checklist item acknowledgment "Missing item" is not defined on the shared checklist item attestation layer.',
+ );
+ });
+
+ it("sorts newer acknowledgment entries ahead of older acknowledgment entries", () => {
+ const olderEntry = createCrossRunAuditCatalogChecklistItemAcknowledgment({
+ catalogEntryId: "catalog_entry_older",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "acknowledged",
+ },
+ ],
+ operatorId: "ops_oncall",
+ scopeId: "ops",
+ timestamp: "2026-04-02T04:00:00.000Z",
+ });
+ const newerEntry = createCrossRunAuditCatalogChecklistItemAcknowledgment({
+ catalogEntryId: "catalog_entry_newer",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "acknowledged",
+ },
+ ],
+ operatorId: "ops_oncall",
+ scopeId: "ops",
+ timestamp: "2026-04-02T04:00:01.000Z",
+ });
+
+ expect(
+ [olderEntry, newerEntry].sort(
+ compareCrossRunAuditCatalogChecklistItemAcknowledgment,
+ ),
+ ).toEqual([newerEntry, olderEntry]);
+ });
+});
diff --git a/packages/replay/src/checklist-item-acknowledgment.ts b/packages/replay/src/checklist-item-acknowledgment.ts
new file mode 100644
index 0000000..df81594
--- /dev/null
+++ b/packages/replay/src/checklist-item-acknowledgment.ts
@@ -0,0 +1,193 @@
+import type {
+ CrossRunAuditCatalogChecklistItemAttestationApplication,
+ CrossRunAuditCatalogChecklistItemAttestationView,
+} from "./checklist-item-attestation";
+
+export type CrossRunAuditCatalogChecklistItemAcknowledgmentState =
+ | "acknowledged"
+ | "unacknowledged";
+
+export interface CrossRunAuditCatalogChecklistItemAcknowledgmentItem {
+ readonly item: string;
+ readonly state: CrossRunAuditCatalogChecklistItemAcknowledgmentState;
+}
+
+export interface CrossRunAuditCatalogChecklistItemAcknowledgment {
+ readonly acknowledgmentNote?: string;
+ readonly catalogEntryId: string;
+ readonly createdAt: string;
+ readonly items: readonly CrossRunAuditCatalogChecklistItemAcknowledgmentItem[];
+ readonly kind: "catalog-checklist-item-acknowledgment";
+ readonly operatorId: string;
+ readonly scopeId: string;
+ readonly updatedAt: string;
+}
+
+export interface CreateCrossRunAuditCatalogChecklistItemAcknowledgmentInput {
+ readonly acknowledgmentNote?: string;
+ readonly catalogEntryId: string;
+ readonly items: readonly CrossRunAuditCatalogChecklistItemAcknowledgmentItem[];
+ readonly operatorId: string;
+ readonly scopeId: string;
+ readonly timestamp: string;
+}
+
+export interface UpdateCrossRunAuditCatalogChecklistItemAcknowledgmentInput {
+ readonly acknowledgmentNote?: string;
+ readonly items: readonly CrossRunAuditCatalogChecklistItemAcknowledgmentItem[];
+}
+
+export interface CrossRunAuditCatalogChecklistItemAcknowledgmentStore {
+ deleteCatalogChecklistItemAcknowledgment(
+ catalogEntryId: string,
+ ): Promise;
+ getCatalogChecklistItemAcknowledgment(
+ catalogEntryId: string,
+ ): Promise;
+ listCatalogChecklistItemAcknowledgments(): Promise<
+ readonly CrossRunAuditCatalogChecklistItemAcknowledgment[]
+ >;
+ saveCatalogChecklistItemAcknowledgment(
+ acknowledgment: CrossRunAuditCatalogChecklistItemAcknowledgment,
+ ): Promise;
+}
+
+export interface CrossRunAuditCatalogChecklistItemAcknowledgmentView {
+ readonly acknowledgment: CrossRunAuditCatalogChecklistItemAcknowledgment;
+ readonly attestation: CrossRunAuditCatalogChecklistItemAttestationView;
+}
+
+export interface CrossRunAuditCatalogChecklistItemAcknowledgmentCollection {
+ readonly items: readonly CrossRunAuditCatalogChecklistItemAcknowledgmentView[];
+ readonly totalCount: number;
+}
+
+export interface CrossRunAuditCatalogChecklistItemAcknowledgmentApplication {
+ readonly application: CrossRunAuditCatalogChecklistItemAttestationApplication;
+ readonly acknowledgment: CrossRunAuditCatalogChecklistItemAcknowledgmentView;
+}
+
+export function createCrossRunAuditCatalogChecklistItemAcknowledgment(
+ input: CreateCrossRunAuditCatalogChecklistItemAcknowledgmentInput,
+): CrossRunAuditCatalogChecklistItemAcknowledgment {
+ const catalogEntryId = input.catalogEntryId.trim();
+ const operatorId = input.operatorId.trim();
+ const scopeId = input.scopeId.trim();
+ const items = normalizeChecklistItemAcknowledgmentItems(input.items);
+ const acknowledgmentNote = normalizeAcknowledgmentNote(
+ input.acknowledgmentNote,
+ );
+
+ if (catalogEntryId.length === 0) {
+ throw new Error(
+ "Catalog checklist item acknowledgments require a catalog entry id.",
+ );
+ }
+
+ if (operatorId.length === 0) {
+ throw new Error(
+ "Catalog checklist item acknowledgments require an operator id.",
+ );
+ }
+
+ if (scopeId.length === 0) {
+ throw new Error(
+ "Catalog checklist item acknowledgments require a scope id.",
+ );
+ }
+
+ if (items.length === 0) {
+ throw new Error(
+ "Catalog checklist item acknowledgments require at least one checklist item acknowledgment entry.",
+ );
+ }
+
+ return {
+ ...(acknowledgmentNote ? { acknowledgmentNote } : {}),
+ catalogEntryId,
+ createdAt: input.timestamp,
+ items,
+ kind: "catalog-checklist-item-acknowledgment",
+ operatorId,
+ scopeId,
+ updatedAt: input.timestamp,
+ };
+}
+
+export function compareCrossRunAuditCatalogChecklistItemAcknowledgment(
+ left: CrossRunAuditCatalogChecklistItemAcknowledgment,
+ right: CrossRunAuditCatalogChecklistItemAcknowledgment,
+): number {
+ return (
+ compareAcknowledgmentPriority(left, right) ||
+ right.updatedAt.localeCompare(left.updatedAt) ||
+ right.createdAt.localeCompare(left.createdAt) ||
+ left.catalogEntryId.localeCompare(right.catalogEntryId)
+ );
+}
+
+export function normalizeChecklistItemAcknowledgmentItems(
+ items:
+ | readonly CrossRunAuditCatalogChecklistItemAcknowledgmentItem[]
+ | undefined,
+ allowedItems?: readonly string[],
+): readonly CrossRunAuditCatalogChecklistItemAcknowledgmentItem[] {
+ const allowedItemSet = allowedItems
+ ? new Set(allowedItems.map((item) => item.trim()).filter(Boolean))
+ : undefined;
+ const dedupedItems = new Map<
+ string,
+ CrossRunAuditCatalogChecklistItemAcknowledgmentState
+ >();
+
+ for (const entry of items ?? []) {
+ const item = entry.item.trim();
+
+ if (item.length === 0) {
+ continue;
+ }
+
+ if (entry.state !== "acknowledged" && entry.state !== "unacknowledged") {
+ throw new Error(
+ `Catalog checklist item acknowledgments require state acknowledged|unacknowledged for "${item}".`,
+ );
+ }
+
+ if (allowedItemSet && !allowedItemSet.has(item)) {
+ throw new Error(
+ `Catalog checklist item acknowledgment "${item}" is not defined on the shared checklist item attestation layer.`,
+ );
+ }
+
+ dedupedItems.set(item, entry.state);
+ }
+
+ return [...dedupedItems.entries()].map(([item, state]) => ({
+ item,
+ state,
+ }));
+}
+
+function compareAcknowledgmentPriority(
+ left: CrossRunAuditCatalogChecklistItemAcknowledgment,
+ right: CrossRunAuditCatalogChecklistItemAcknowledgment,
+): number {
+ const leftPriority = left.items.some((item) => item.state === "acknowledged")
+ ? 0
+ : 1;
+ const rightPriority = right.items.some(
+ (item) => item.state === "acknowledged",
+ )
+ ? 0
+ : 1;
+
+ return leftPriority - rightPriority;
+}
+
+function normalizeAcknowledgmentNote(
+ value: string | undefined,
+): string | undefined {
+ const normalizedValue = value?.trim();
+
+ return normalizedValue ? normalizedValue : undefined;
+}
diff --git a/packages/replay/src/index.ts b/packages/replay/src/index.ts
index a22d60d..faf4beb 100644
--- a/packages/replay/src/index.ts
+++ b/packages/replay/src/index.ts
@@ -34,6 +34,20 @@ export {
createCrossRunAuditCatalogEntry,
type PublishCrossRunAuditCatalogEntryInput,
} from "./catalog";
+export {
+ type CreateCrossRunAuditCatalogChecklistItemAcknowledgmentInput,
+ type CrossRunAuditCatalogChecklistItemAcknowledgment,
+ type CrossRunAuditCatalogChecklistItemAcknowledgmentApplication,
+ type CrossRunAuditCatalogChecklistItemAcknowledgmentCollection,
+ type CrossRunAuditCatalogChecklistItemAcknowledgmentItem,
+ type CrossRunAuditCatalogChecklistItemAcknowledgmentState,
+ type CrossRunAuditCatalogChecklistItemAcknowledgmentStore,
+ type CrossRunAuditCatalogChecklistItemAcknowledgmentView,
+ compareCrossRunAuditCatalogChecklistItemAcknowledgment,
+ createCrossRunAuditCatalogChecklistItemAcknowledgment,
+ normalizeChecklistItemAcknowledgmentItems,
+ type UpdateCrossRunAuditCatalogChecklistItemAcknowledgmentInput,
+} from "./checklist-item-acknowledgment";
export {
type CreateCrossRunAuditCatalogChecklistItemAttestationInput,
type CrossRunAuditCatalogChecklistItemAttestation,
@@ -150,6 +164,7 @@ export {
} from "./navigation";
export {
type CrossRunAuditCatalogAssignmentChecklistQuery,
+ type CrossRunAuditCatalogChecklistItemAcknowledgmentQuery,
type CrossRunAuditCatalogChecklistItemAttestationQuery,
type CrossRunAuditCatalogChecklistItemBlockerQuery,
type CrossRunAuditCatalogChecklistItemEvidenceQuery,
@@ -166,6 +181,7 @@ export {
type CrossRunAuditReader,
type CrossRunAuditSavedViewQuery,
createCrossRunAuditCatalogAssignmentChecklistQuery,
+ createCrossRunAuditCatalogChecklistItemAcknowledgmentQuery,
createCrossRunAuditCatalogChecklistItemAttestationQuery,
createCrossRunAuditCatalogChecklistItemBlockerQuery,
createCrossRunAuditCatalogChecklistItemEvidenceQuery,
diff --git a/packages/replay/src/query.ts b/packages/replay/src/query.ts
index 532bf2f..83414d7 100644
--- a/packages/replay/src/query.ts
+++ b/packages/replay/src/query.ts
@@ -25,6 +25,17 @@ import {
createCrossRunAuditCatalogEntry,
type PublishCrossRunAuditCatalogEntryInput,
} from "./catalog";
+import {
+ type CrossRunAuditCatalogChecklistItemAcknowledgment,
+ type CrossRunAuditCatalogChecklistItemAcknowledgmentApplication,
+ type CrossRunAuditCatalogChecklistItemAcknowledgmentCollection,
+ type CrossRunAuditCatalogChecklistItemAcknowledgmentStore,
+ type CrossRunAuditCatalogChecklistItemAcknowledgmentView,
+ compareCrossRunAuditCatalogChecklistItemAcknowledgment,
+ createCrossRunAuditCatalogChecklistItemAcknowledgment,
+ normalizeChecklistItemAcknowledgmentItems,
+ type UpdateCrossRunAuditCatalogChecklistItemAcknowledgmentInput,
+} from "./checklist-item-acknowledgment";
import {
type CrossRunAuditCatalogChecklistItemAttestation,
type CrossRunAuditCatalogChecklistItemAttestationApplication,
@@ -433,6 +444,32 @@ export interface CrossRunAuditCatalogChecklistItemEvidenceQuery {
): Promise;
}
+export interface CrossRunAuditCatalogChecklistItemAcknowledgmentQuery {
+ applyCatalogEntry(
+ id: string,
+ viewer: CrossRunAuditCatalogVisibilityViewer,
+ ): Promise<
+ CrossRunAuditCatalogChecklistItemAcknowledgmentApplication | undefined
+ >;
+ clearCatalogChecklistItemAcknowledgment(
+ id: string,
+ viewer: CrossRunAuditCatalogVisibilityViewer,
+ ): Promise;
+ getCatalogChecklistItemAcknowledgment(
+ id: string,
+ viewer: CrossRunAuditCatalogVisibilityViewer,
+ ): Promise;
+ listAcknowledgedCatalogEntries(
+ viewer: CrossRunAuditCatalogVisibilityViewer,
+ ): Promise;
+ setCatalogChecklistItemAcknowledgment(
+ id: string,
+ viewer: CrossRunAuditCatalogVisibilityViewer,
+ input: UpdateCrossRunAuditCatalogChecklistItemAcknowledgmentInput,
+ timestamp: string,
+ ): Promise;
+}
+
export interface CrossRunAuditCatalogChecklistItemAttestationQuery {
applyCatalogEntry(
id: string,
@@ -2100,6 +2137,161 @@ export function createCrossRunAuditCatalogChecklistItemAttestationQuery(
};
}
+export function createCrossRunAuditCatalogChecklistItemAcknowledgmentQuery(
+ store: CrossRunAuditCatalogChecklistItemAcknowledgmentStore,
+ attestationEntries: CrossRunAuditCatalogChecklistItemAttestationQuery,
+): CrossRunAuditCatalogChecklistItemAcknowledgmentQuery {
+ return {
+ async applyCatalogEntry(id, viewer) {
+ const acknowledgment =
+ await resolveCatalogChecklistItemAcknowledgmentView(
+ store,
+ attestationEntries,
+ id,
+ viewer,
+ );
+
+ if (!acknowledgment) {
+ return undefined;
+ }
+
+ const application = await attestationEntries.applyCatalogEntry(
+ id,
+ viewer,
+ );
+
+ if (!application) {
+ return undefined;
+ }
+
+ return {
+ acknowledgment,
+ application,
+ };
+ },
+
+ async clearCatalogChecklistItemAcknowledgment(id, viewer) {
+ const acknowledgment =
+ await resolveCatalogChecklistItemAcknowledgmentView(
+ store,
+ attestationEntries,
+ id,
+ viewer,
+ );
+
+ if (!acknowledgment) {
+ return undefined;
+ }
+
+ await store.deleteCatalogChecklistItemAcknowledgment(id);
+
+ return acknowledgment;
+ },
+
+ async getCatalogChecklistItemAcknowledgment(id, viewer) {
+ return resolveCatalogChecklistItemAcknowledgmentView(
+ store,
+ attestationEntries,
+ id,
+ viewer,
+ );
+ },
+
+ async listAcknowledgedCatalogEntries(viewer) {
+ const acknowledgmentEntries = [
+ ...(await store.listCatalogChecklistItemAcknowledgments()),
+ ].sort(compareCrossRunAuditCatalogChecklistItemAcknowledgment);
+ const items = (
+ await Promise.all(
+ acknowledgmentEntries.map(async (acknowledgment) =>
+ resolveCatalogChecklistItemAcknowledgmentFromValue(
+ attestationEntries,
+ acknowledgment,
+ viewer,
+ ),
+ ),
+ )
+ ).filter(
+ (
+ acknowledgment,
+ ): acknowledgment is CrossRunAuditCatalogChecklistItemAcknowledgmentView =>
+ acknowledgment !== undefined,
+ );
+
+ return {
+ items,
+ totalCount: items.length,
+ };
+ },
+
+ async setCatalogChecklistItemAcknowledgment(id, viewer, input, timestamp) {
+ const attestation =
+ await attestationEntries.getCatalogChecklistItemAttestation(id, viewer);
+
+ if (!attestation) {
+ throw new Error(
+ `Catalog entry "${id}" is not attested and visible to operator "${viewer.operatorId}".`,
+ );
+ }
+
+ const allowedItems = attestation.attestation.items.map(
+ (item) => item.item,
+ );
+ const normalizedItems = normalizeChecklistItemAcknowledgmentItems(
+ input.items,
+ allowedItems,
+ );
+
+ if (normalizedItems.length === 0) {
+ throw new Error(
+ "Catalog checklist item acknowledgments require at least one checklist item acknowledgment entry.",
+ );
+ }
+
+ const existingAcknowledgment =
+ await store.getCatalogChecklistItemAcknowledgment(id);
+ const normalizedAcknowledgmentNote = input.acknowledgmentNote?.trim();
+ const acknowledgment = existingAcknowledgment
+ ? {
+ ...(input.acknowledgmentNote === undefined
+ ? existingAcknowledgment.acknowledgmentNote
+ ? {
+ acknowledgmentNote:
+ existingAcknowledgment.acknowledgmentNote,
+ }
+ : {}
+ : normalizedAcknowledgmentNote
+ ? { acknowledgmentNote: normalizedAcknowledgmentNote }
+ : {}),
+ catalogEntryId: id,
+ createdAt: existingAcknowledgment.createdAt,
+ items: normalizedItems,
+ kind: existingAcknowledgment.kind,
+ operatorId: viewer.operatorId,
+ scopeId: viewer.scopeId,
+ updatedAt: timestamp,
+ }
+ : createCrossRunAuditCatalogChecklistItemAcknowledgment({
+ ...(normalizedAcknowledgmentNote
+ ? { acknowledgmentNote: normalizedAcknowledgmentNote }
+ : {}),
+ catalogEntryId: id,
+ items: normalizedItems,
+ operatorId: viewer.operatorId,
+ scopeId: viewer.scopeId,
+ timestamp,
+ });
+
+ await store.saveCatalogChecklistItemAcknowledgment(acknowledgment);
+
+ return {
+ acknowledgment,
+ attestation,
+ };
+ },
+ };
+}
+
async function resolveCatalogEntryView(
store: CrossRunAuditCatalogStore,
savedViews: CrossRunAuditSavedViewQuery,
@@ -2575,3 +2767,56 @@ async function resolveCatalogChecklistItemAttestationFromValue(
evidence,
};
}
+
+async function resolveCatalogChecklistItemAcknowledgmentView(
+ store: CrossRunAuditCatalogChecklistItemAcknowledgmentStore,
+ attestationQuery: CrossRunAuditCatalogChecklistItemAttestationQuery,
+ id: string,
+ viewer: CrossRunAuditCatalogVisibilityViewer,
+): Promise {
+ const acknowledgment = await store.getCatalogChecklistItemAcknowledgment(id);
+
+ return acknowledgment
+ ? resolveCatalogChecklistItemAcknowledgmentFromValue(
+ attestationQuery,
+ acknowledgment,
+ viewer,
+ )
+ : undefined;
+}
+
+async function resolveCatalogChecklistItemAcknowledgmentFromValue(
+ attestationQuery: CrossRunAuditCatalogChecklistItemAttestationQuery,
+ acknowledgment: CrossRunAuditCatalogChecklistItemAcknowledgment,
+ viewer: CrossRunAuditCatalogVisibilityViewer,
+): Promise {
+ if (acknowledgment.scopeId !== viewer.scopeId) {
+ return undefined;
+ }
+
+ const attestation = await attestationQuery.getCatalogChecklistItemAttestation(
+ acknowledgment.catalogEntryId,
+ viewer,
+ );
+
+ if (!attestation) {
+ return undefined;
+ }
+
+ const normalizedItems = normalizeChecklistItemAcknowledgmentItems(
+ acknowledgment.items,
+ attestation.attestation.items.map((item) => item.item),
+ );
+
+ if (normalizedItems.length === 0) {
+ return undefined;
+ }
+
+ return {
+ acknowledgment: {
+ ...acknowledgment,
+ items: normalizedItems,
+ },
+ attestation,
+ };
+}
diff --git a/packages/sdk/src/errors.ts b/packages/sdk/src/errors.ts
index b0309c4..01bd868 100644
--- a/packages/sdk/src/errors.ts
+++ b/packages/sdk/src/errors.ts
@@ -29,6 +29,7 @@ export class OperatorNotFoundError extends OperatorError {
resource:
| "approval"
| "catalog assignment checklist"
+ | "catalog checklist item acknowledgment"
| "catalog checklist item blocker"
| "catalog checklist item attestation"
| "catalog checklist item evidence"
diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts
index 42e6d3a..0070a89 100644
--- a/packages/sdk/src/index.ts
+++ b/packages/sdk/src/index.ts
@@ -7,6 +7,7 @@ export {
OperatorNotFoundError,
} from "./errors";
export {
+ type AcknowledgeAuditCatalogEntryInput,
type AssignAuditCatalogEntryInput,
type AttestAuditCatalogEntryInput,
type BlockAuditCatalogEntryInput,
diff --git a/packages/sdk/src/operator-service.integration.test.ts b/packages/sdk/src/operator-service.integration.test.ts
index 5908b94..6d33b58 100644
--- a/packages/sdk/src/operator-service.integration.test.ts
+++ b/packages/sdk/src/operator-service.integration.test.ts
@@ -2594,4 +2594,352 @@ describe("@runroot/sdk operator service integration", () => {
);
expect(peerAttestationsAfterClear.totalCount).toBe(0);
});
+
+ it("acknowledges, lists-acknowledged, inspects, clears, and reapplies attested presets for inline and queued runs through the operator seam", async () => {
+ const workspaceRoot = await mkdtemp(
+ join(tmpdir(), "runroot-sdk-checklist-acknowledgment-"),
+ );
+ const sqlitePath = join(workspaceRoot, "runroot.sqlite");
+ const now = createClock();
+ const idGenerator = createIdGenerator();
+ let savedViewCount = 0;
+ let catalogEntryCount = 0;
+ const inlineService = createRunrootOperatorService({
+ executionMode: "inline",
+ idGenerator,
+ now,
+ persistenceDriver: "sqlite",
+ sqlitePath,
+ });
+ const queuedService = createRunrootOperatorService({
+ executionMode: "queued",
+ idGenerator,
+ now,
+ persistenceDriver: "sqlite",
+ sqlitePath,
+ });
+ const worker = createRunrootWorkerService({
+ idGenerator,
+ now,
+ persistenceDriver: "sqlite",
+ sqlitePath,
+ workerId: "worker_checklist_acknowledgments",
+ });
+ const ownerService = createRunrootOperatorService({
+ catalogEntryIdGenerator: () =>
+ `catalog_entry_acknowledgment_${++catalogEntryCount}`,
+ idGenerator,
+ now,
+ operatorId: "ops_oncall",
+ operatorScopeId: "ops",
+ persistenceDriver: "sqlite",
+ savedViewIdGenerator: () =>
+ `saved_view_acknowledgment_${++savedViewCount}`,
+ sqlitePath,
+ });
+ const peerService = createRunrootOperatorService({
+ idGenerator,
+ now,
+ operatorId: "ops_backup",
+ operatorScopeId: "ops",
+ persistenceDriver: "sqlite",
+ sqlitePath,
+ });
+
+ const inlineRun = await inlineService.startRun({
+ input: {
+ approvalRequired: false,
+ commandAlias: "print-ready",
+ runbookId: "node-health-check",
+ },
+ templateId: "shell-runbook-flow",
+ });
+ const queuedRun = await queuedService.startRun({
+ input: {
+ approvalRequired: false,
+ commandAlias: "print-ready",
+ runbookId: "node-health-check",
+ },
+ templateId: "shell-runbook-flow",
+ });
+
+ await worker.processNextJob();
+
+ const inlineSavedView = await ownerService.saveSavedView({
+ description: "Inline acknowledgment preset for owner follow-up",
+ name: "Inline acknowledgment preset",
+ navigation: {
+ drilldown: {
+ runId: inlineRun.id,
+ },
+ summary: {
+ executionMode: "inline",
+ },
+ },
+ refs: {
+ auditViewRunId: inlineRun.id,
+ drilldownRunId: inlineRun.id,
+ },
+ });
+ const queuedSavedView = await ownerService.saveSavedView({
+ description: "Queued acknowledgment preset for shared follow-up",
+ name: "Queued acknowledgment preset",
+ navigation: {
+ drilldown: {
+ workerId: "worker_checklist_acknowledgments",
+ },
+ summary: {
+ executionMode: "queued",
+ },
+ },
+ refs: {
+ auditViewRunId: queuedRun.id,
+ drilldownRunId: queuedRun.id,
+ },
+ });
+ const inlineCatalogEntry = await ownerService.publishCatalogEntry({
+ savedViewId: inlineSavedView.id,
+ });
+ const queuedCatalogEntry = await ownerService.publishCatalogEntry({
+ savedViewId: queuedSavedView.id,
+ });
+
+ await ownerService.shareCatalogEntry(queuedCatalogEntry.entry.id);
+ await ownerService.reviewCatalogEntry(inlineCatalogEntry.entry.id, {
+ note: "Inline acknowledgment preset verified by the owner",
+ state: "reviewed",
+ });
+ await peerService.reviewCatalogEntry(queuedCatalogEntry.entry.id, {
+ note: "Queued acknowledgment preset ready for backup follow-up",
+ state: "recommended",
+ });
+ await ownerService.assignCatalogEntry(inlineCatalogEntry.entry.id, {
+ assigneeId: "ops_oncall",
+ handoffNote: "Inline acknowledgment preset remains with the owner",
+ });
+ await ownerService.assignCatalogEntry(queuedCatalogEntry.entry.id, {
+ assigneeId: "ops_backup",
+ handoffNote: "Queued acknowledgment preset handed to the backup operator",
+ });
+ await ownerService.checklistCatalogEntry(inlineCatalogEntry.entry.id, {
+ items: ["Confirm inline owner follow-up"],
+ state: "completed",
+ });
+ await ownerService.checklistCatalogEntry(queuedCatalogEntry.entry.id, {
+ items: ["Validate queued follow-up", "Close backup handoff"],
+ state: "pending",
+ });
+ await ownerService.progressCatalogEntry(inlineCatalogEntry.entry.id, {
+ items: [
+ {
+ item: "Confirm inline owner follow-up",
+ state: "completed",
+ },
+ ],
+ });
+ await ownerService.progressCatalogEntry(queuedCatalogEntry.entry.id, {
+ completionNote: "Queued follow-up is almost complete",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "completed",
+ },
+ {
+ item: "Close backup handoff",
+ state: "pending",
+ },
+ ],
+ });
+ await ownerService.blockCatalogEntry(inlineCatalogEntry.entry.id, {
+ items: [
+ {
+ item: "Confirm inline owner follow-up",
+ state: "blocked",
+ },
+ ],
+ });
+ await ownerService.blockCatalogEntry(queuedCatalogEntry.entry.id, {
+ blockerNote: "Waiting for the overnight handoff",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "cleared",
+ },
+ {
+ item: "Close backup handoff",
+ state: "blocked",
+ },
+ ],
+ });
+ await ownerService.resolveCatalogEntry(inlineCatalogEntry.entry.id, {
+ items: [
+ {
+ item: "Confirm inline owner follow-up",
+ state: "resolved",
+ },
+ ],
+ });
+ await ownerService.resolveCatalogEntry(queuedCatalogEntry.entry.id, {
+ resolutionNote: "Backup confirmed the follow-up closure",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "resolved",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unresolved",
+ },
+ ],
+ });
+ await ownerService.verifyCatalogEntry(inlineCatalogEntry.entry.id, {
+ items: [
+ {
+ item: "Confirm inline owner follow-up",
+ state: "verified",
+ },
+ ],
+ });
+ await ownerService.verifyCatalogEntry(queuedCatalogEntry.entry.id, {
+ verificationNote: "Backup verified the follow-up closure",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "verified",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unverified",
+ },
+ ],
+ });
+ await ownerService.recordCatalogEntryEvidence(inlineCatalogEntry.entry.id, {
+ items: [
+ {
+ item: "Confirm inline owner follow-up",
+ references: ["run://inline-follow-up"],
+ },
+ ],
+ });
+ await ownerService.recordCatalogEntryEvidence(queuedCatalogEntry.entry.id, {
+ evidenceNote: "Backup collected stable follow-up references",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ references: ["run://queued-follow-up", "note://backup-closeout"],
+ },
+ {
+ item: "Close backup handoff",
+ references: ["doc://backup-handoff"],
+ },
+ ],
+ });
+ await ownerService.attestCatalogEntry(inlineCatalogEntry.entry.id, {
+ items: [
+ {
+ item: "Confirm inline owner follow-up",
+ state: "attested",
+ },
+ ],
+ });
+ await ownerService.attestCatalogEntry(queuedCatalogEntry.entry.id, {
+ attestationNote: "Backup attested the stable follow-up evidence",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "attested",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unattested",
+ },
+ ],
+ });
+ await ownerService.acknowledgeCatalogEntry(inlineCatalogEntry.entry.id, {
+ items: [
+ {
+ item: "Confirm inline owner follow-up",
+ state: "acknowledged",
+ },
+ ],
+ });
+ await ownerService.acknowledgeCatalogEntry(queuedCatalogEntry.entry.id, {
+ acknowledgmentNote: "Backup acknowledged the attested follow-up",
+ items: [
+ {
+ item: "Validate queued follow-up",
+ state: "acknowledged",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unacknowledged",
+ },
+ ],
+ });
+
+ const ownerAcknowledgments =
+ await ownerService.listAcknowledgedCatalogEntries();
+ const peerAcknowledgments =
+ await peerService.listAcknowledgedCatalogEntries();
+ const inspectedAcknowledgment =
+ await ownerService.getCatalogChecklistItemAcknowledgment(
+ queuedCatalogEntry.entry.id,
+ );
+ const appliedAcknowledgment = await peerService.applyCatalogEntry(
+ queuedCatalogEntry.entry.id,
+ );
+ const clearedAcknowledgment =
+ await ownerService.clearCatalogChecklistItemAcknowledgment(
+ queuedCatalogEntry.entry.id,
+ );
+ const peerAcknowledgmentsAfterClear =
+ await peerService.listAcknowledgedCatalogEntries();
+
+ expect(
+ ownerAcknowledgments.items.map(
+ (item) =>
+ item.attestation.evidence.verification.resolution.blocker.progress
+ .checklist.assignment.review.visibility.catalogEntry.entry.id,
+ ),
+ ).toEqual([queuedCatalogEntry.entry.id, inlineCatalogEntry.entry.id]);
+ expect(
+ peerAcknowledgments.items.map(
+ (item) =>
+ item.attestation.evidence.verification.resolution.blocker.progress
+ .checklist.assignment.review.visibility.catalogEntry.entry.id,
+ ),
+ ).toEqual([queuedCatalogEntry.entry.id]);
+ expect(inspectedAcknowledgment.acknowledgment).toMatchObject({
+ acknowledgmentNote: "Backup acknowledged the attested follow-up",
+ catalogEntryId: queuedCatalogEntry.entry.id,
+ operatorId: "ops_oncall",
+ scopeId: "ops",
+ });
+ expect(inspectedAcknowledgment.acknowledgment.items).toEqual([
+ {
+ item: "Validate queued follow-up",
+ state: "acknowledged",
+ },
+ {
+ item: "Close backup handoff",
+ state: "unacknowledged",
+ },
+ ]);
+ expect(appliedAcknowledgment.catalogEntry.entry.id).toBe(
+ queuedCatalogEntry.entry.id,
+ );
+ expect(appliedAcknowledgment.application.savedView.id).toBe(
+ queuedSavedView.id,
+ );
+ expect(appliedAcknowledgment.application.navigation.totalSummaryCount).toBe(
+ 1,
+ );
+ expect(
+ appliedAcknowledgment.application.navigation.drilldowns[0]?.result.runId,
+ ).toBe(queuedRun.id);
+ expect(clearedAcknowledgment.acknowledgment.catalogEntryId).toBe(
+ queuedCatalogEntry.entry.id,
+ );
+ expect(peerAcknowledgmentsAfterClear.totalCount).toBe(0);
+ });
});
diff --git a/packages/sdk/src/operator-service.ts b/packages/sdk/src/operator-service.ts
index 29c2aed..cdb20e1 100644
--- a/packages/sdk/src/operator-service.ts
+++ b/packages/sdk/src/operator-service.ts
@@ -19,6 +19,7 @@ import type { JsonValue, WorkflowRun } from "@runroot/domain";
import type { RunrootLogger, RunrootTracer } from "@runroot/observability";
import {
createConfiguredAuditCatalogAssignmentChecklistStore,
+ createConfiguredAuditCatalogChecklistItemAcknowledgmentStore,
createConfiguredAuditCatalogChecklistItemAttestationStore,
createConfiguredAuditCatalogChecklistItemBlockerStore,
createConfiguredAuditCatalogChecklistItemEvidenceStore,
@@ -40,6 +41,10 @@ import {
type CrossRunAuditCatalogAssignmentChecklistState,
type CrossRunAuditCatalogAssignmentChecklistStore,
type CrossRunAuditCatalogAssignmentChecklistView,
+ type CrossRunAuditCatalogChecklistItemAcknowledgmentCollection,
+ type CrossRunAuditCatalogChecklistItemAcknowledgmentItem,
+ type CrossRunAuditCatalogChecklistItemAcknowledgmentStore,
+ type CrossRunAuditCatalogChecklistItemAcknowledgmentView,
type CrossRunAuditCatalogChecklistItemAttestationCollection,
type CrossRunAuditCatalogChecklistItemAttestationItem,
type CrossRunAuditCatalogChecklistItemAttestationStore,
@@ -91,6 +96,7 @@ import {
type CrossRunAuditSavedViewNavigationRefs,
type CrossRunAuditSavedViewStore,
createCrossRunAuditCatalogAssignmentChecklistQuery,
+ createCrossRunAuditCatalogChecklistItemAcknowledgmentQuery,
createCrossRunAuditCatalogChecklistItemAttestationQuery,
createCrossRunAuditCatalogChecklistItemBlockerQuery,
createCrossRunAuditCatalogChecklistItemEvidenceQuery,
@@ -206,6 +212,11 @@ export interface AttestAuditCatalogEntryInput {
readonly items: readonly CrossRunAuditCatalogChecklistItemAttestationItem[];
}
+export interface AcknowledgeAuditCatalogEntryInput {
+ readonly acknowledgmentNote?: string;
+ readonly items: readonly CrossRunAuditCatalogChecklistItemAcknowledgmentItem[];
+}
+
export interface ReviewAuditCatalogEntryInput {
readonly note?: string;
readonly state: CrossRunAuditCatalogReviewState;
@@ -234,6 +245,10 @@ export interface RunrootOperatorService {
id: string,
input: AttestAuditCatalogEntryInput,
): Promise;
+ acknowledgeCatalogEntry(
+ id: string,
+ input: AcknowledgeAuditCatalogEntryInput,
+ ): Promise;
recordCatalogEntryEvidence(
id: string,
input: RecordAuditCatalogEntryEvidenceInput,
@@ -255,6 +270,9 @@ export interface RunrootOperatorService {
clearCatalogChecklistItemAttestation(
id: string,
): Promise;
+ clearCatalogChecklistItemAcknowledgment(
+ id: string,
+ ): Promise;
clearCatalogChecklistItemEvidence(
id: string,
): Promise;
@@ -282,6 +300,9 @@ export interface RunrootOperatorService {
getCatalogChecklistItemAttestation(
id: string,
): Promise;
+ getCatalogChecklistItemAcknowledgment(
+ id: string,
+ ): Promise;
getCatalogChecklistItemEvidence(
id: string,
): Promise;
@@ -319,6 +340,7 @@ export interface RunrootOperatorService {
filters?: CrossRunAuditDrilldownFilters,
): Promise;
listAssignedCatalogEntries(): Promise;
+ listAcknowledgedCatalogEntries(): Promise;
listBlockedCatalogEntries(): Promise;
listChecklistedCatalogEntries(): Promise;
listCatalogEntries(): Promise;
@@ -358,6 +380,7 @@ export interface RunrootOperatorService {
export interface RunrootOperatorServiceOptions {
readonly approvalIdGenerator?: () => string;
readonly catalogAssignmentChecklistStore?: CrossRunAuditCatalogAssignmentChecklistStore;
+ readonly catalogChecklistItemAcknowledgmentStore?: CrossRunAuditCatalogChecklistItemAcknowledgmentStore;
readonly catalogChecklistItemAttestationStore?: CrossRunAuditCatalogChecklistItemAttestationStore;
readonly catalogChecklistItemBlockerStore?: CrossRunAuditCatalogChecklistItemBlockerStore;
readonly catalogChecklistItemEvidenceStore?: CrossRunAuditCatalogChecklistItemEvidenceStore;
@@ -578,6 +601,19 @@ export function createRunrootOperatorService(
? { workspacePath: options.workspacePath }
: {}),
});
+ const catalogChecklistItemAcknowledgmentStore =
+ options.catalogChecklistItemAcknowledgmentStore ??
+ createConfiguredAuditCatalogChecklistItemAcknowledgmentStore({
+ ...(options.databaseUrl ? { databaseUrl: options.databaseUrl } : {}),
+ ...(options.env ? { env: options.env } : {}),
+ ...(options.persistenceDriver
+ ? { driver: options.persistenceDriver }
+ : {}),
+ ...(options.sqlitePath ? { sqlitePath: options.sqlitePath } : {}),
+ ...(options.workspacePath
+ ? { workspacePath: options.workspacePath }
+ : {}),
+ });
const catalogChecklistItemAttestationStore =
options.catalogChecklistItemAttestationStore ??
createConfiguredAuditCatalogChecklistItemAttestationStore({
@@ -731,6 +767,11 @@ export function createRunrootOperatorService(
catalogChecklistItemAttestationStore,
crossRunAuditCatalogChecklistItemEvidence,
);
+ const crossRunAuditCatalogChecklistItemAcknowledgments =
+ createCrossRunAuditCatalogChecklistItemAcknowledgmentQuery(
+ catalogChecklistItemAcknowledgmentStore,
+ crossRunAuditCatalogChecklistItemAttestations,
+ );
const persistenceLocation = persistenceConfig.location;
return {
@@ -823,6 +864,19 @@ export function createRunrootOperatorService(
}
},
+ async acknowledgeCatalogEntry(id, input) {
+ try {
+ return await crossRunAuditCatalogChecklistItemAcknowledgments.setCatalogChecklistItemAcknowledgment(
+ id,
+ operatorIdentity,
+ input,
+ now(),
+ );
+ } catch (error) {
+ throw normalizeCatalogChecklistItemAcknowledgmentError(error, id);
+ }
+ },
+
async recordCatalogEntryEvidence(id, input) {
try {
return await crossRunAuditCatalogChecklistItemEvidence.setCatalogChecklistItemEvidence(
@@ -929,6 +983,23 @@ export function createRunrootOperatorService(
return attestation;
},
+ async clearCatalogChecklistItemAcknowledgment(id) {
+ const acknowledgment =
+ await crossRunAuditCatalogChecklistItemAcknowledgments.clearCatalogChecklistItemAcknowledgment(
+ id,
+ operatorIdentity,
+ );
+
+ if (!acknowledgment) {
+ throw new OperatorNotFoundError(
+ "catalog checklist item acknowledgment",
+ id,
+ );
+ }
+
+ return acknowledgment;
+ },
+
async clearCatalogChecklistItemEvidence(id) {
const evidence =
await crossRunAuditCatalogChecklistItemEvidence.clearCatalogChecklistItemEvidence(
@@ -1118,6 +1189,23 @@ export function createRunrootOperatorService(
return attestation;
},
+ async getCatalogChecklistItemAcknowledgment(id) {
+ const acknowledgment =
+ await crossRunAuditCatalogChecklistItemAcknowledgments.getCatalogChecklistItemAcknowledgment(
+ id,
+ operatorIdentity,
+ );
+
+ if (!acknowledgment) {
+ throw new OperatorNotFoundError(
+ "catalog checklist item acknowledgment",
+ id,
+ );
+ }
+
+ return acknowledgment;
+ },
+
async getCatalogChecklistItemEvidence(id) {
const evidence =
await crossRunAuditCatalogChecklistItemEvidence.getCatalogChecklistItemEvidence(
@@ -1281,6 +1369,12 @@ export function createRunrootOperatorService(
);
},
+ async listAcknowledgedCatalogEntries() {
+ return crossRunAuditCatalogChecklistItemAcknowledgments.listAcknowledgedCatalogEntries(
+ operatorIdentity,
+ );
+ },
+
async listEvidencedCatalogEntries() {
return crossRunAuditCatalogChecklistItemEvidence.listEvidencedCatalogEntries(
operatorIdentity,
@@ -1924,6 +2018,46 @@ function normalizeCatalogChecklistItemAttestationError(
return normalizeOperatorError(error);
}
+function normalizeCatalogChecklistItemAcknowledgmentError(
+ error: unknown,
+ catalogEntryId: string,
+): Error {
+ if (
+ error instanceof OperatorConflictError ||
+ error instanceof OperatorInputError ||
+ error instanceof OperatorNotFoundError
+ ) {
+ return error;
+ }
+
+ if (
+ error instanceof Error &&
+ (error.message.startsWith(
+ "Catalog checklist item acknowledgments require",
+ ) ||
+ error.message.startsWith(`Catalog checklist item acknowledgment "`) ||
+ error.message.startsWith(
+ `Catalog entry "${catalogEntryId}" does not define checklist item attestation entries`,
+ ))
+ ) {
+ return new OperatorInputError(error.message);
+ }
+
+ if (
+ error instanceof Error &&
+ error.message.startsWith(
+ `Catalog entry "${catalogEntryId}" is not attested and visible to operator`,
+ )
+ ) {
+ return new OperatorNotFoundError(
+ "catalog checklist item acknowledgment",
+ catalogEntryId,
+ );
+ }
+
+ return normalizeOperatorError(error);
+}
+
function normalizeCatalogReviewSignalError(
error: unknown,
catalogEntryId: string,