diff --git a/packages/db/migrations/0012_deliverable_git.sql b/packages/db/migrations/0012_deliverable_git.sql
new file mode 100644
index 0000000..2fe5182
--- /dev/null
+++ b/packages/db/migrations/0012_deliverable_git.sql
@@ -0,0 +1,2 @@
+ALTER TABLE "deliverables" ADD COLUMN "commit_sha" text DEFAULT '' NOT NULL;--> statement-breakpoint
+ALTER TABLE "deliverables" ADD COLUMN "git_ref" text DEFAULT '' NOT NULL;
diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json
index 61daf5d..6814dfa 100644
--- a/packages/db/migrations/meta/_journal.json
+++ b/packages/db/migrations/meta/_journal.json
@@ -64,6 +64,13 @@
"when": 1747500005000,
"tag": "0011_kpi_snapshots",
"breakpoints": false
+ },
+ {
+ "idx": 9,
+ "version": "7",
+ "when": 1747500006000,
+ "tag": "0012_deliverable_git",
+ "breakpoints": false
}
]
}
diff --git a/packages/db/src/schema/deliverables.ts b/packages/db/src/schema/deliverables.ts
index bbbb698..68918b2 100644
--- a/packages/db/src/schema/deliverables.ts
+++ b/packages/db/src/schema/deliverables.ts
@@ -46,6 +46,12 @@ export const deliverables = pgTable(
sizeBytes: bigint("size_bytes", { mode: "number" }).notNull().default(0),
contentHash: text("content_hash").notNull().default(""),
+ // Git-first verifiability — the artifact is committed to a per-company
+ // deliverables repo so `git -C
show ` is the proof.
+ // commitSha is the canonical content-address; gitRef is the branch/ref.
+ commitSha: text("commit_sha").notNull().default(""),
+ gitRef: text("git_ref").notNull().default(""),
+
// What it is
title: text("title").notNull(),
description: text("description").notNull().default(""),
diff --git a/packages/shared/src/types/mission-control.ts b/packages/shared/src/types/mission-control.ts
index e177ef1..07d4faa 100644
--- a/packages/shared/src/types/mission-control.ts
+++ b/packages/shared/src/types/mission-control.ts
@@ -177,6 +177,11 @@ export interface Deliverable {
sizeBytes: number;
contentHash: string;
+ // Git-first verifiability — the artifact is committed to a per-company
+ // deliverables repo; commitSha is the proof, gitRef the branch/ref.
+ commitSha?: string;
+ gitRef?: string;
+
// What it is
title: string;
description: string;
@@ -204,7 +209,12 @@ export type DeliverableStatus =
| "in_review"
| "approved"
| "rejected"
- | "published";
+ | "published"
+ // Git-artifact integrity outcomes (matches the cloud deliverable_ledger
+ // vocabulary): `verified` = commit resolves + content hash matches;
+ // `failed` = verification could not confirm the artifact.
+ | "verified"
+ | "failed";
export type DeliverableKind =
| "document"
@@ -349,6 +359,7 @@ export type ActivityEventKind =
| "deliverable_revised"
| "deliverable_approved"
| "deliverable_published"
+ | "deliverable_verified"
// Node lifecycle
| "node_paused"
| "node_resumed"
diff --git a/packages/wavex-os-server/src/mission-control/deliverables.ts b/packages/wavex-os-server/src/mission-control/deliverables.ts
index bd74580..c567c6e 100644
--- a/packages/wavex-os-server/src/mission-control/deliverables.ts
+++ b/packages/wavex-os-server/src/mission-control/deliverables.ts
@@ -15,9 +15,13 @@
* knowing the layout; `writeDeliverable()` computes the path. */
import { randomUUID, createHash } from "node:crypto";
-import { mkdir, writeFile } from "node:fs/promises";
+import { mkdir, writeFile, readFile, stat } from "node:fs/promises";
+import { execFile } from "node:child_process";
+import { promisify } from "node:util";
import { dirname, join } from "node:path";
import { and, desc, eq } from "drizzle-orm";
+
+const execFileAsync = promisify(execFile);
import {
type Db,
deliverables,
@@ -89,6 +93,53 @@ function deliverableActionForKind(kind: DeliverableKind): string {
}
const PRODUCED_EVENT: ActivityEventKind = "deliverable_produced";
+const VERIFIED_EVENT: ActivityEventKind = "deliverable_verified";
+
+// Inline committer identity so we never depend on the operator's global
+// git config (which may be unset on a fresh box). Local-only repo.
+const GIT_IDENT = [
+ "-c", "user.email=deliverables@wavex-os.local",
+ "-c", "user.name=WaveX Deliverables",
+];
+
+async function gitDir(dir: string): Promise {
+ try {
+ await stat(join(dir, ".git"));
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+/** Commit one artifact file into the per-company deliverables git repo so
+ * it is verifiable via `git -C show `. Lazily `git init`s the
+ * repo on first write. Best-effort: returns null on any git failure so
+ * the caller still records the DB row (the row is the source of truth;
+ * the commit is the proof layer). */
+async function commitArtifact(
+ dir: string,
+ relPath: string,
+ message: string,
+): Promise<{ commitSha: string; gitRef: string } | null> {
+ try {
+ if (!(await gitDir(dir))) {
+ await execFileAsync("git", ["init", "-q"], { cwd: dir });
+ // Pin a deterministic default branch — older git defaults to master.
+ await execFileAsync("git", ["symbolic-ref", "HEAD", "refs/heads/main"], { cwd: dir }).catch(() => {});
+ }
+ await execFileAsync("git", ["add", "--", relPath], { cwd: dir });
+ await execFileAsync(
+ "git",
+ [...GIT_IDENT, "commit", "-q", "-m", message, "--", relPath],
+ { cwd: dir },
+ );
+ const { stdout: sha } = await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: dir });
+ const { stdout: ref } = await execFileAsync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: dir });
+ return { commitSha: sha.trim(), gitRef: ref.trim() };
+ } catch {
+ return null;
+ }
+}
/** Write a Deliverable row + JSON envelope to disk + emit MC event.
* Returns the canonical Deliverable shape. Best-effort disk write — a
@@ -126,9 +177,22 @@ export async function writeDeliverable(
const contentHash = createHash("sha256").update(envelope).digest("hex");
const sizeBytes = Buffer.byteLength(envelope, "utf8");
+ let commitSha = "";
+ let gitRef = "";
try {
await mkdir(dir, { recursive: true });
await writeFile(diskPath, envelope, "utf8");
+ // Git-first: commit the artifact so it is verifiable. Best-effort —
+ // a git failure leaves commitSha empty and the row is still written.
+ const committed = await commitArtifact(
+ dir,
+ filename,
+ `deliverable(${input.kind}): ${input.title} [${input.taskRefType}:${input.taskRefId}]`,
+ );
+ if (committed) {
+ commitSha = committed.commitSha;
+ gitRef = committed.gitRef;
+ }
} catch {
// Disk write is best-effort — the DB row is the canonical record.
}
@@ -145,6 +209,8 @@ export async function writeDeliverable(
relPath: filename,
sizeBytes,
contentHash,
+ commitSha,
+ gitRef,
title: input.title,
description,
previewText: input.previewText ?? null,
@@ -242,6 +308,8 @@ function rowToDeliverable(row: typeof deliverables.$inferSelect): DeliverableSha
relPath: row.relPath,
sizeBytes: row.sizeBytes,
contentHash: row.contentHash,
+ commitSha: row.commitSha || undefined,
+ gitRef: row.gitRef || undefined,
title: row.title,
description: row.description,
previewText: row.previewText ?? undefined,
@@ -273,3 +341,94 @@ export async function deliverableFolder(
if (!d || !d.diskPath) return null;
return { folder: dirname(d.diskPath), diskPath: d.diskPath };
}
+
+export interface VerifyResult {
+ ok: boolean;
+ /** Final status written to the row: "verified" on success, "failed" otherwise. */
+ status: DeliverableStatus;
+ /** Human-readable reason when verification fails. */
+ reason?: string;
+ deliverable: DeliverableShape;
+}
+
+/** Verify a deliverable's git artifact: (1) the recorded commit resolves
+ * in the company's deliverables repo, and (2) the on-disk file still
+ * hashes to the recorded contentHash. Sets status `verified` / `failed`,
+ * stamps reviewedAt, and emits a `deliverable_verified` event. Returns
+ * null if the deliverable id is unknown. */
+export async function verifyDeliverable(
+ id: string,
+ reviewerNodeId: string | undefined,
+ db?: Db,
+): Promise {
+ const resolved = db ?? (await getDb());
+ const rows = await resolved
+ .select()
+ .from(deliverables)
+ .where(eq(deliverables.id, id))
+ .limit(1);
+ const row = rows[0];
+ if (!row) return null;
+
+ const dir = dirname(row.diskPath);
+ const failures: string[] = [];
+
+ // (1) Commit must resolve in the deliverables repo.
+ if (row.commitSha) {
+ try {
+ await execFileAsync("git", ["cat-file", "-e", `${row.commitSha}^{commit}`], { cwd: dir });
+ } catch {
+ failures.push(`commit ${row.commitSha.slice(0, 10)} not found in deliverables repo`);
+ }
+ } else {
+ failures.push("no commit_sha recorded (artifact was not git-committed)");
+ }
+
+ // (2) On-disk content must still match the recorded hash.
+ try {
+ const buf = await readFile(row.diskPath);
+ const actual = createHash("sha256").update(buf).digest("hex");
+ if (actual !== row.contentHash) {
+ failures.push("content hash mismatch — artifact changed since it was recorded");
+ }
+ } catch {
+ failures.push("artifact file missing on disk");
+ }
+
+ const status: DeliverableStatus = failures.length === 0 ? "verified" : "failed";
+ const reason = failures.length ? failures.join("; ") : undefined;
+ const reviewedAt = new Date();
+
+ const updated = await resolved
+ .update(deliverables)
+ .set({
+ status,
+ reviewedByNodeId: reviewerNodeId ?? null,
+ reviewedAt,
+ reviewNotes: reason ?? null,
+ })
+ .where(eq(deliverables.id, id))
+ .returning();
+ const stored = updated[0] ?? row;
+ const deliverable = rowToDeliverable(stored);
+
+ const modeContext: PaperclipMode = row.diskPath.includes("/avatars/")
+ ? "avatar"
+ : "solo_founder";
+ await logMissionControlActivity({
+ companyId: row.companyId,
+ instanceId: row.instanceId,
+ kind: VERIFIED_EVENT,
+ modeContext,
+ actorNodeId: reviewerNodeId ?? row.producedByNodeId,
+ action: `deliverable.${row.kind}.${status}`,
+ subjectRef: { kind: "deliverable", id, title: row.title },
+ deliverableRef: { id, title: row.title, kind: row.kind as DeliverableKind },
+ plainLanguageSentence:
+ status === "verified"
+ ? `Verified deliverable "${row.title}" — commit resolves and content is intact.`
+ : `Could not verify deliverable "${row.title}": ${reason}.`,
+ });
+
+ return { ok: status === "verified", status, reason, deliverable };
+}
diff --git a/packages/wavex-os-server/src/routes/mission-control.ts b/packages/wavex-os-server/src/routes/mission-control.ts
index 6ce190e..69d23c7 100644
--- a/packages/wavex-os-server/src/routes/mission-control.ts
+++ b/packages/wavex-os-server/src/routes/mission-control.ts
@@ -25,7 +25,10 @@ import {
deliverableFolder,
getDeliverable,
queryDeliverables,
+ writeDeliverable,
+ verifyDeliverable,
type QueryDeliverablesInput,
+ type WriteDeliverableInput,
} from "../mission-control/deliverables.js";
import {
announceDueImpacts,
@@ -542,6 +545,97 @@ export function registerMissionControlRoutes(app: FastifyInstance): void {
}
});
+ // ── POST /api/mission-control/deliverable ─────────────────────────────
+ // Producer write-path: an agent run records its output as a git-committed,
+ // verifiable artifact. Body must carry the task linkage + the artifact
+ // (payload). writeDeliverable() commits it to the company's deliverables
+ // repo and returns the row + commit_sha.
+ app.post("/api/mission-control/deliverable", async (req, reply) => {
+ const ar = authReq(req);
+ try {
+ assertBoard(ar);
+ } catch (e) {
+ if (e instanceof AuthError)
+ return reply.status(e.statusCode).send({ error: e.message });
+ throw e;
+ }
+ const body = (req.body ?? {}) as Partial;
+ const missing = (["instanceId", "taskRefId", "producedByNodeId", "kind", "title"] as const).filter(
+ (k) => !body[k],
+ );
+ if (missing.length) {
+ return reply.status(400).send({ ok: false, error: `missing required fields: ${missing.join(", ")}` });
+ }
+ assertCompanyAccess(ar, body.instanceId as string);
+ await ensureBootstrap();
+ try {
+ const deliverable = await writeDeliverable({
+ companyId: body.companyId ?? (body.instanceId as string),
+ instanceId: body.instanceId as string,
+ modeContext: body.modeContext ?? "solo_founder",
+ taskRefType: body.taskRefType ?? "issue",
+ taskRefId: body.taskRefId as string,
+ producedByNodeId: body.producedByNodeId as string,
+ kind: body.kind as WriteDeliverableInput["kind"],
+ title: body.title as string,
+ description: body.description,
+ previewText: body.previewText,
+ mimeType: body.mimeType,
+ payload: body.payload,
+ templateUsed: body.templateUsed,
+ promptUsedRef: body.promptUsedRef,
+ expectedKpiImpactId: body.expectedKpiImpactId,
+ filename: body.filename,
+ taskRef: body.taskRef,
+ plainLanguageSentence: body.plainLanguageSentence,
+ });
+ return { ok: true, deliverable, commit_sha: deliverable.commitSha ?? null };
+ } catch (e) {
+ if (e instanceof AuthError)
+ return reply.status(e.statusCode).send({ error: e.message });
+ return reply.status(503).send({ ok: false, error: e instanceof Error ? e.message : String(e) });
+ }
+ });
+
+ // ── POST /api/mission-control/deliverable/:id/verify ──────────────────
+ // Confirms the recorded commit resolves in the deliverables repo AND the
+ // on-disk artifact still hashes to contentHash. Flips status verified /
+ // failed. This is what turns "the fleet ran" into "the output is real".
+ app.post("/api/mission-control/deliverable/:id/verify", async (req, reply) => {
+ const ar = authReq(req);
+ try {
+ assertBoard(ar);
+ } catch (e) {
+ if (e instanceof AuthError)
+ return reply.status(e.statusCode).send({ error: e.message });
+ throw e;
+ }
+ const { id } = req.params as { id: string };
+ const body = (req.body ?? {}) as { reviewerNodeId?: string };
+ await ensureBootstrap();
+ try {
+ const existing = await getDeliverable(id);
+ if (!existing) {
+ return reply.status(404).send({ ok: false, error: "not found" });
+ }
+ assertCompanyAccess(ar, existing.instanceId);
+ const result = await verifyDeliverable(id, body.reviewerNodeId);
+ if (!result) {
+ return reply.status(404).send({ ok: false, error: "not found" });
+ }
+ return {
+ ok: result.ok,
+ status: result.status,
+ reason: result.reason ?? null,
+ deliverable: result.deliverable,
+ };
+ } catch (e) {
+ if (e instanceof AuthError)
+ return reply.status(e.statusCode).send({ error: e.message });
+ return reply.status(503).send({ ok: false, error: e instanceof Error ? e.message : String(e) });
+ }
+ });
+
// ── KPI impacts ───────────────────────────────────────────────────────
app.post(
"/api/mission-control/:companyId/kpi-impacts",
diff --git a/packages/wavex-os-server/test/mission-control-deliverables.test.ts b/packages/wavex-os-server/test/mission-control-deliverables.test.ts
index 63cc110..6d6ffb6 100644
--- a/packages/wavex-os-server/test/mission-control-deliverables.test.ts
+++ b/packages/wavex-os-server/test/mission-control-deliverables.test.ts
@@ -1,7 +1,9 @@
/** Mission Control Deliverables — write + query round-trip. */
import { afterEach, beforeEach, describe, expect, it } from "vitest";
-import { mkdtempSync, rmSync, readFileSync, existsSync } from "node:fs";
+import { mkdtempSync, rmSync, readFileSync, writeFileSync, existsSync } from "node:fs";
+import { execFileSync } from "node:child_process";
+import { dirname } from "node:path";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { _resetDbCache, runMigrations } from "@wavex-os/db";
@@ -9,6 +11,7 @@ import {
writeDeliverable,
queryDeliverables,
deliverableFolder,
+ verifyDeliverable,
} from "../src/mission-control/deliverables.js";
import {
_resetActivityBusForTesting,
@@ -117,3 +120,82 @@ describe("writeDeliverable", () => {
expect(f!.folder).toMatch(/deliverables$/);
});
});
+
+describe("git-first deliverable artifact", () => {
+ it("commits the artifact and the commit resolves via git", async () => {
+ const d = await writeDeliverable({
+ companyId: "co-git",
+ instanceId: "co-git",
+ modeContext: "solo_founder",
+ taskRefType: "issue",
+ taskRefId: "WAV-100",
+ producedByNodeId: "agent-cmo",
+ kind: "document",
+ title: "Q3 campaign brief",
+ payload: { body: "Launch the autumn line via IG + email." },
+ });
+
+ // commit_sha is recorded (git sha1 = 40 hex, sha256 = 64 hex).
+ expect(d.commitSha).toMatch(/^[a-f0-9]{40,64}$/);
+ expect(d.gitRef).toBe("main");
+
+ // The recorded commit resolves in the deliverables repo, and the file
+ // is part of that commit.
+ const dir = dirname(d.diskPath);
+ expect(() =>
+ execFileSync("git", ["cat-file", "-e", `${d.commitSha}^{commit}`], { cwd: dir }),
+ ).not.toThrow();
+ const tree = execFileSync("git", ["show", "--stat", "--oneline", d.commitSha!], {
+ cwd: dir,
+ encoding: "utf8",
+ });
+ expect(tree).toContain(d.relPath);
+ });
+
+ it("verifyDeliverable flips a clean artifact to verified", async () => {
+ const received: string[] = [];
+ subscribeMissionControlEvents((e) => received.push(e.kind));
+
+ const d = await writeDeliverable({
+ companyId: "co-v",
+ instanceId: "co-v",
+ modeContext: "solo_founder",
+ taskRefType: "issue",
+ taskRefId: "WAV-200",
+ producedByNodeId: "agent-strategy",
+ kind: "document",
+ title: "GTM plan",
+ payload: { body: "Plan body." },
+ });
+
+ const result = await verifyDeliverable(d.id, "reviewer-cto");
+ expect(result).not.toBeNull();
+ expect(result!.ok).toBe(true);
+ expect(result!.status).toBe("verified");
+ expect(result!.deliverable.status).toBe("verified");
+ expect(result!.deliverable.reviewedByNodeId).toBe("reviewer-cto");
+ expect(received).toContain("deliverable_verified");
+ });
+
+ it("verifyDeliverable fails when the on-disk artifact is tampered", async () => {
+ const d = await writeDeliverable({
+ companyId: "co-t",
+ instanceId: "co-t",
+ modeContext: "solo_founder",
+ taskRefType: "issue",
+ taskRefId: "WAV-300",
+ producedByNodeId: "agent-x",
+ kind: "document",
+ title: "Tamper test",
+ payload: { body: "original" },
+ });
+
+ // Mutate the artifact on disk after it was recorded + committed.
+ writeFileSync(d.diskPath, JSON.stringify({ tampered: true }), "utf8");
+
+ const result = await verifyDeliverable(d.id, undefined);
+ expect(result!.ok).toBe(false);
+ expect(result!.status).toBe("failed");
+ expect(result!.reason).toMatch(/hash mismatch/i);
+ });
+});