feat: deliverable review gate, app lifecycle fixes, CI/CD pipeline

- Add deliverable review gate: pause workflow when agents produce important
  documents (PRDs, design docs, etc.) for human review before continuing
- Add company-level reviewDeliverables setting with onboarding card selector
- Block agent heartbeats when deliverable review is pending (agent + CEO)
- Trigger review at document creation time in API server for reliability
- Fix autoApproveHires vs reviewDeliverables conflict in approval-policy
- Fix app quit: force-exit on all platforms (macOS Dock, Windows tray, Linux)
- Add multi-platform CI/CD pipeline (macOS arm64/x64, Windows x64/arm64,
  Linux x64/arm64 with deb/AppImage/rpm/snap) with code signing support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.6 committed 2026-03-23 23:31:48 -04:00
1 parent 9e2c19259a
commit d52985f61e
24 files changed
+737 -48

No files matched your search

+133
View File
@@ -10,6 +10,12 @@ import type {
import { dispatchAgentMessage } from "./message-dispatch";
import type { NotificationOptions } from "./notification-service";
/** Document types considered important enough to gate behind user review when reviewDeliverables is enabled. */
const IMPORTANT_DOCUMENT_TYPES = new Set([
"prd", "technical_spec", "design_doc", "project_brief", "proposal",
"budget_proposal", "architecture_decision", "contract", "sop",
]);
const WORKFLOW_ACTIONS: ReadonlySet<AutomationAction> = new Set([
"assign_task",
"create_task",
@@ -801,6 +807,57 @@ export function createAutomationService({
return false;
}
/** Returns important documents created during this run (matching IMPORTANT_DOCUMENT_TYPES). */
function findImportantDocsForRun(runId: string, companyId: string): Array<{ id: string; type: string; title: string }> {
try {
const placeholders = [...IMPORTANT_DOCUMENT_TYPES].map(() => "?").join(", ");
return db.queryAll<{ id: string; type: string; title: string }>(
`select id, type, title from documents where company_id = ? and type in (${placeholders}) and created_at >= (select started_at from runs where id = ?)`,
companyId, ...IMPORTANT_DOCUMENT_TYPES, runId,
);
} catch { return []; }
}
/** Creates a deliverable_review approval and fires OS notification. */
function createDeliverableReviewApproval(
companyId: string,
taskId: string,
agentId: string,
docs: Array<{ id: string; type: string; title: string }>,
reviewerNote?: string,
): string {
const docTitles = docs.map((d) => d.title).join(", ");
const approvalId = db.requestApproval({
companyId,
relatedTaskId: taskId,
requestedByAgentId: agentId,
type: "deliverable_review",
payloadSummary: `Review deliverable: ${docTitles}`.slice(0, 280),
impactSummary: reviewerNote ?? "Agent completed work with important deliverables. Awaiting your final review.",
payloadJson: JSON.stringify({ documentIds: docs.map((d) => d.id), documents: docs }),
});
db.setTaskRequiresUserReview(taskId, false);
// Pause the producing agent's heartbeat until the user reviews
try {
const agent = db.getAgent(agentId);
if (agent.heartbeatEnabled) {
db.setHeartbeat(agentId, false, agent.heartbeatIntervalSec || 120);
logger.info(`[deliverable-review] Paused heartbeat for ${agent.name} — awaiting user review.`);
}
} catch { /* best effort */ }
try {
notify({
title: "Deliverable Ready for Review",
body: `${docTitles}`.slice(0, 200),
urgency: "informational",
navigation: { section: "approvals", entityId: approvalId },
batchKey: "deliverable_review",
});
} catch { /* best effort */ }
publishDomainChanged();
return approvalId;
}
function processRunCompletion(run: RunRecord, event: RunFinishedEvent) {
let task: ReturnType<typeof db.getTask> | null = null;
try {
@@ -841,6 +898,27 @@ export function createAutomationService({
db.advanceTaskStatus(task.id, "in_review", "Run completed with deliverable. Awaiting review.");
handleTaskStatusChange(task.id, run.companyId, "in_progress", "in_review");
effectiveStatus = "in_review";
// Check if deliverable review gate should activate
try {
const company = db.listSnapshot().companies.find((c) => c.id === run.companyId);
if (company?.reviewDeliverables) {
const importantDocs = findImportantDocsForRun(run.id, run.companyId);
if (importantDocs.length > 0) {
db.setTaskRequiresUserReview(task.id, true);
const agent = db.getAgent(run.agentId);
// Pause heartbeat immediately — agent should not run while deliverable awaits review
if (agent.heartbeatEnabled) {
db.setHeartbeat(agent.id, false, agent.heartbeatIntervalSec || 120);
logger.info(`[deliverable-review] Paused heartbeat for ${agent.name} — deliverable flagged for review.`);
}
// If agent has no manager (is CEO), create approval immediately
const chain = db.getChainOfCommand(agent.id);
if (chain.length === 0) {
createDeliverableReviewApproval(run.companyId, task.id, run.agentId, importantDocs);
}
}
}
} catch { /* deliverable review gate is best-effort */ }
publishDomainChanged();
}
} else {
@@ -848,6 +926,25 @@ export function createAutomationService({
db.advanceTaskStatus(task.id, "in_review", "Run completed successfully. Awaiting review.");
handleTaskStatusChange(task.id, run.companyId, "in_progress", "in_review");
effectiveStatus = "in_review";
// Check if deliverable review gate should activate for code/general tasks too
try {
const company = db.listSnapshot().companies.find((c) => c.id === run.companyId);
if (company?.reviewDeliverables) {
const importantDocs = findImportantDocsForRun(run.id, run.companyId);
if (importantDocs.length > 0) {
db.setTaskRequiresUserReview(task.id, true);
const agent = db.getAgent(run.agentId);
if (agent.heartbeatEnabled) {
db.setHeartbeat(agent.id, false, agent.heartbeatIntervalSec || 120);
logger.info(`[deliverable-review] Paused heartbeat for ${agent.name} — deliverable flagged for review.`);
}
const chain = db.getChainOfCommand(agent.id);
if (chain.length === 0) {
createDeliverableReviewApproval(run.companyId, task.id, run.agentId, importantDocs);
}
}
}
} catch { /* deliverable review gate is best-effort */ }
publishDomainChanged();
}
}
@@ -976,6 +1073,42 @@ export function createAutomationService({
}
}
if (approval.type === "deliverable_review" && approval.relatedTaskId) {
try {
const task = db.getTask(approval.relatedTaskId);
// Re-enable heartbeat for the producing agent now that the user has decided
if (task.assigneeAgentId) {
try {
const agent = db.getAgent(task.assigneeAgentId);
if (!agent.heartbeatEnabled) {
db.setHeartbeat(task.assigneeAgentId, true, agent.heartbeatIntervalSec || 120);
logger.info(`[deliverable-review] Resumed heartbeat for ${agent.name} after user ${decision}.`);
}
} catch { /* best effort */ }
}
if (decision === "approved") {
db.advanceTaskStatus(task.id, "done", "Board approved deliverable.");
handleTaskStatusChange(task.id, companyId, task.status, "done");
db.addActivity({ companyId, actor: "board", action: "deliverable.approved", entityType: "task", entityId: task.id, detail: approval.decisionNote || "Deliverable approved by board." });
publishDomainChanged();
} else if (decision === "rejected") {
db.advanceTaskStatus(task.id, "in_progress", `Board rejected deliverable: ${approval.decisionNote || "No reason given."}`);
handleTaskStatusChange(task.id, companyId, task.status, "in_progress");
if (task.assigneeAgentId) {
wakeAgentIfPossible(task.assigneeAgentId, companyId, "revision_needed");
}
publishDomainChanged();
} else if (decision === "revision_requested") {
if (task.assigneeAgentId) {
wakeAgentIfPossible(task.assigneeAgentId, companyId, "revision_needed");
}
publishDomainChanged();
}
} catch {
// Deliverable review follow-up is best-effort.
}
}
const browserAction = db.getBrowserActionByApprovalId(approvalId, companyId);
if (browserAction) {
if (decision === "approved") {