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

+227
View File
@@ -0,0 +1,227 @@
name: Build & Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
publish:
description: 'Publish to GitHub Releases'
required: false
type: boolean
default: false
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
env:
PNPM_VERSION: '10.24.0'
NODE_VERSION: '22'
jobs:
# ─────────────────────────────────────────────
# Stage 1: Lint & Typecheck (fast feedback)
# ─────────────────────────────────────────────
check:
name: Lint & Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm exec tsc --noEmit
# ─────────────────────────────────────────────
# Stage 2: Unit Tests
# ─────────────────────────────────────────────
test:
name: Unit Tests
runs-on: ubuntu-latest
needs: check
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test
# ─────────────────────────────────────────────
# Stage 3: Build matrix (parallel per platform)
# ─────────────────────────────────────────────
build-macos:
name: macOS (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
needs: test
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
runner: macos-14
builder_args: --mac --arm64
- arch: x64
runner: macos-13
builder_args: --mac --x64
env:
CSC_LINK: ${{ secrets.MAC_CERTIFICATE_P12_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
CSC_IDENTITY_AUTO_DISCOVERY: 'true'
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Build electron-vite
run: pnpm exec electron-vite build
- name: Package & sign
run: pnpm exec electron-builder ${{ matrix.builder_args }} --config electron-builder.yml --publish never
- uses: actions/upload-artifact@v4
with:
name: macos-${{ matrix.arch }}
path: |
build/release/*.dmg
build/release/*.zip
build/release/*.blockmap
build/release/latest-mac.yml
if-no-files-found: error
build-windows:
name: Windows (${{ matrix.arch }})
runs-on: windows-latest
needs: test
strategy:
fail-fast: false
matrix:
include:
- arch: x64
builder_args: --win --x64
- arch: arm64
builder_args: --win --arm64
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Build electron-vite
run: pnpm exec electron-vite build
- name: Package
run: pnpm exec electron-builder ${{ matrix.builder_args }} --config electron-builder.yml --publish never
env:
CSC_LINK: ${{ secrets.WIN_CERTIFICATE_P12_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.WIN_CERTIFICATE_PASSWORD }}
- uses: actions/upload-artifact@v4
with:
name: windows-${{ matrix.arch }}
path: |
build/release/*.exe
build/release/*.blockmap
build/release/latest.yml
if-no-files-found: error
build-linux:
name: Linux (${{ matrix.arch }})
runs-on: ubuntu-latest
needs: test
strategy:
fail-fast: false
matrix:
include:
- arch: x64
builder_args: --linux --x64
- arch: arm64
builder_args: --linux --arm64
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Install snapcraft (x64 only)
if: matrix.arch == 'x64'
run: sudo snap install snapcraft --classic
- name: Build electron-vite
run: pnpm exec electron-vite build
- name: Package
run: pnpm exec electron-builder ${{ matrix.builder_args }} --config electron-builder.yml --publish never
- uses: actions/upload-artifact@v4
with:
name: linux-${{ matrix.arch }}
path: |
build/release/*.AppImage
build/release/*.deb
build/release/*.rpm
build/release/*.snap
build/release/*.blockmap
build/release/latest-linux.yml
if-no-files-found: warn
# ─────────────────────────────────────────────
# Stage 4: Publish to GitHub Releases
# ─────────────────────────────────────────────
release:
name: Publish Release
runs-on: ubuntu-latest
needs: [build-macos, build-windows, build-linux]
if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.publish == 'true'
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: false
- name: Collect release files
run: |
mkdir -p release-files
find artifacts -type f \( \
-name "*.dmg" -o -name "*.zip" -o -name "*.exe" \
-o -name "*.AppImage" -o -name "*.deb" -o -name "*.rpm" \
-o -name "*.snap" -o -name "*.blockmap" \
-o -name "latest*.yml" \
\) -exec cp {} release-files/ \;
ls -lh release-files/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
draft: true
generate_release_notes: true
files: release-files/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+9
View File
@@ -14,6 +14,15 @@ win-unpacked
test-results/
playwright-report/
# Signing certificates (NEVER commit these)
*.p12
*.cer
*.pem
*.key
*.p8
*.provisionprofile
*.mobileprovision
# Environment files
.env
.env.local
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
+60 -5
View File
@@ -8,23 +8,78 @@ files:
- package.json
asar: true
icon: assets/icon.png
mac:
category: public.app-category.developer-tools
icon: assets/icon.icns
target:
- dmg
- zip
- target: dmg
arch:
- x64
- arm64
- target: zip
arch:
- x64
- arm64
hardenedRuntime: true
gatekeeperAssess: false
entitlements: assets/entitlements.mac.plist
entitlementsInherit: assets/entitlements.mac.plist
notarize: false
win:
icon: assets/icon.ico
target:
- nsis
- portable
- target: nsis
arch:
- x64
- arm64
- target: portable
arch:
- x64
linux:
icon: assets/icon.png
target:
- AppImage
- target: AppImage
arch:
- x64
- arm64
- target: deb
arch:
- x64
- arm64
- target: rpm
arch:
- x64
- target: snap
arch:
- x64
category: Development
nsis:
oneClick: false
perMachine: false
allowToChangeInstallationDirectory: true
deb:
depends:
- libgtk-3-0
- libnotify4
- libnss3
- libxss1
- libxtst6
- xdg-utils
- libatspi2.0-0
- libuuid1
- libsecret-1-0
snap:
plugs:
- default
- removable-media
publish:
provider: github
owner: ${env.GH_REPO_OWNER}
repo: ${env.GH_REPO_NAME}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "agent-company",
"version": "0.4.80",
"version": "0.4.90",
"private": true,
"type": "module",
"description": "Cross-platform Electron desktop control plane for local-first AI agent companies.",
+76 -2
View File
@@ -390,6 +390,47 @@ export class AgentApiServer {
this.json(res, 403, { error: "Current agent is not allowed to assign this task to the selected agent" });
return;
}
// Deliverable review gate: intercept agent trying to advance a flagged task to "done"
let deliverableReviewIntercepted = false;
if (update.status === "done" && task.requiresUserReview) {
try {
const importantTypes = ["prd","technical_spec","design_doc","project_brief","proposal","budget_proposal","architecture_decision","contract","sop"];
const placeholders = importantTypes.map(() => "?").join(", ");
// Scope to documents authored by the task's agent, created during this task's lifetime
const docs = this.db.queryAll<{ id: string; type: string; title: string }>(
`select id, type, title from documents where company_id = ? and type in (${placeholders})
and (author_agent_id = ? or created_at >= (select created_at from tasks where id = ?))
order by created_at desc limit 10`,
task.companyId, ...importantTypes, task.assigneeAgentId ?? "", task.id,
);
const reviewerAgent = this.db.getAgent(token.agentId);
const docTitles = docs.map((d) => d.title).join(", ");
this.db.requestApproval({
companyId: task.companyId,
relatedTaskId: task.id,
requestedByAgentId: token.agentId,
type: "deliverable_review",
payloadSummary: `Review deliverable: ${docTitles || task.title}`.slice(0, 280),
impactSummary: `${reviewerAgent.name} reviewed and approved. Awaiting your final review.`.slice(0, 400),
payloadJson: JSON.stringify({ documentIds: docs.map((d) => d.id), documents: docs }),
});
this.db.setTaskRequiresUserReview(task.id, false);
// Pause the producing agent's heartbeat until user reviews
if (task.assigneeAgentId) {
try {
const prodAgent = this.db.getAgent(task.assigneeAgentId);
if (prodAgent.heartbeatEnabled) {
this.db.setHeartbeat(task.assigneeAgentId, false, prodAgent.heartbeatIntervalSec || 120);
}
} catch { /* best effort */ }
}
this.db.addComment({ companyId: task.companyId, taskId: task.id, authorAgentId: null, authorName: "System", body: "Deliverable submitted for board review." });
update.status = "in_review"; // Override: keep in review, don't advance to done
deliverableReviewIntercepted = true;
} catch { /* deliverable review interception is best-effort; fall through to normal flow */ }
}
const input: Record<string, unknown> = {
id: task.id,
companyId: task.companyId,
@@ -1393,10 +1434,12 @@ export class AgentApiServer {
this.json(res, 400, { error: "Goal does not belong to the current company" });
return;
}
const docType = String(data.type ?? "knowledge_article");
const docTitle = String(data.title ?? "Untitled");
const id = this.db.saveDocument({
companyId: token.companyId,
type: String(data.type ?? "knowledge_article"),
title: String(data.title ?? "Untitled"),
type: docType,
title: docTitle,
content: String(data.content ?? ""),
authorAgentId: token.agentId,
reviewerAgentId,
@@ -1412,6 +1455,37 @@ export class AgentApiServer {
if (reviewerAgentId) {
this.onWakeAgent(reviewerAgentId, token.companyId, "assignment");
}
// Deliverable review gate: check at document creation time (most reliable)
const IMPORTANT_DOC_TYPES = new Set(["prd","technical_spec","design_doc","project_brief","proposal","budget_proposal","architecture_decision","contract","sop"]);
if (IMPORTANT_DOC_TYPES.has(docType)) {
try {
const company = this.db.listSnapshot().companies.find((c) => c.id === token.companyId);
if (company?.reviewDeliverables) {
// Check if there's already a pending deliverable_review for this company
if (!this.db.hasPendingDeliverableReviews(token.companyId)) {
this.db.requestApproval({
companyId: token.companyId,
relatedTaskId: null,
requestedByAgentId: token.agentId,
type: "deliverable_review",
payloadSummary: `Review deliverable: ${docTitle}`.slice(0, 280),
impactSummary: `New ${docType.replaceAll("_", " ")} created. Awaiting your review before workflow continues.`.slice(0, 400),
payloadJson: JSON.stringify({ documentIds: [id], documents: [{ id, type: docType, title: docTitle }] }),
});
// Pause the producing agent's heartbeat
try {
const agent = this.db.getAgent(token.agentId);
if (agent.heartbeatEnabled) {
this.db.setHeartbeat(token.agentId, false, agent.heartbeatIntervalSec || 120);
}
} catch { /* best effort */ }
this.onDomainChanged();
}
}
} catch { /* deliverable review gate is best-effort */ }
}
this.json(res, 201, { id });
});
+4 -4
View File
@@ -10,7 +10,7 @@ interface AppLifecycleDependencies {
export function createAppLifecycleService({
getMainWindow,
ensureWindow,
shouldQuit: _shouldQuit,
shouldQuit,
onBeforeQuit,
}: AppLifecycleDependencies) {
function register(hasSingleInstanceLock: boolean) {
@@ -44,9 +44,9 @@ export function createAppLifecycleService({
});
app.on("window-all-closed", () => {
// On macOS, keep the app running when all windows are closed (standard behavior).
// On Windows and Linux, quit the app.
if (process.platform !== "darwin") {
// On macOS, keep the app running when all windows are closed (standard behavior)
// UNLESS the user explicitly chose Quit (before-quit already fired).
if (process.platform !== "darwin" || shouldQuit()) {
app.quit();
}
});
+11 -2
View File
@@ -11,10 +11,19 @@ export function shouldAutoApproveApproval(
return false;
}
return db
const company = db
.listSnapshot()
.companies
.some((company) => company.id === companyId && company.autoApproveHires);
.find((c) => c.id === companyId);
if (!company?.autoApproveHires) return false;
// When reviewDeliverables is ON, CEO strategy needs human review — don't auto-approve
if (approvalType === "approve_ceo_strategy" && company.reviewDeliverables) {
return false;
}
return true;
}
export function resolveApprovalDecision({
+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") {
+54 -10
View File
@@ -528,8 +528,8 @@ export class AppDatabase {
secretBackend: this.vault.getBackendLabel(),
},
companies: this.readAll<CompanyRecord>(
"select id, name, description, status, auto_approve_hires as autoApproveHires, created_at as createdAt, updated_at as updatedAt from companies order by created_at asc",
).map((record) => ({ ...record, autoApproveHires: toBoolean(record.autoApproveHires) })) as CompanyRecord[],
"select id, name, description, status, auto_approve_hires as autoApproveHires, review_deliverables as reviewDeliverables, created_at as createdAt, updated_at as updatedAt from companies order by created_at asc",
).map((record) => ({ ...record, autoApproveHires: toBoolean(record.autoApproveHires), reviewDeliverables: toBoolean(record.reviewDeliverables) })) as CompanyRecord[],
connectors,
workspaces: this.readAll<WorkspaceRecord>(
`select id, company_id as companyId, name, local_path as localPath, repo_url as repoUrl, repo_ref as repoRef, project_id as projectId, state, is_primary as isPrimary, created_at as createdAt, updated_at as updatedAt from workspaces${companyWhere} order by created_at desc`,
@@ -548,9 +548,9 @@ export class AppDatabase {
...companyArgs,
),
tasks: this.readAll<TaskRecord>(
`select id, company_id as companyId, project_id as projectId, goal_id as goalId, parent_id as parentId, title, description, assignee_agent_id as assigneeAgentId, workspace_id as workspaceId, status, priority, active_run_id as activeRunId, session_json as sessionJson, session_display_id as sessionDisplayId, cost_usd as costUsd, created_at as createdAt, updated_at as updatedAt from tasks${companyWhere} order by created_at desc`,
`select id, company_id as companyId, project_id as projectId, goal_id as goalId, parent_id as parentId, title, description, assignee_agent_id as assigneeAgentId, workspace_id as workspaceId, status, priority, active_run_id as activeRunId, session_json as sessionJson, session_display_id as sessionDisplayId, cost_usd as costUsd, requires_user_review as requiresUserReview, created_at as createdAt, updated_at as updatedAt from tasks${companyWhere} order by created_at desc`,
...companyArgs,
),
).map((record) => ({ ...record, requiresUserReview: toBoolean(record.requiresUserReview) })) as TaskRecord[],
approvals: this.readAll<ApprovalRecord>(
`select id, company_id as companyId, related_task_id as relatedTaskId, related_agent_id as relatedAgentId, requested_by_agent_id as requestedByAgentId, type, payload_summary as payloadSummary, impact_summary as impactSummary, payload_json as payloadJson, decision_note as decisionNote, state, created_at as createdAt, updated_at as updatedAt from approvals${companyWhere} order by created_at desc`,
...companyArgs,
@@ -675,19 +675,20 @@ export class AppDatabase {
return snapshot;
}
saveCompany(input: { id?: string | null; name: string; description: string; status: string; autoApproveHires?: boolean }) {
saveCompany(input: { id?: string | null; name: string; description: string; status: string; autoApproveHires?: boolean; reviewDeliverables?: boolean }) {
const id = ensureId(input.id ?? null);
const ts = nowIso();
const autoApprove = input.autoApproveHires ? 1 : 0;
const reviewDeliverables = input.reviewDeliverables ? 1 : 0;
const existing = this.db.prepare("select id from companies where id = ?").get(id) as { id: string } | undefined;
if (existing) {
this.db
.prepare("update companies set name = ?, description = ?, status = ?, auto_approve_hires = ?, updated_at = ? where id = ?")
.run(input.name, input.description, input.status, autoApprove, ts, id);
.prepare("update companies set name = ?, description = ?, status = ?, auto_approve_hires = ?, review_deliverables = ?, updated_at = ? where id = ?")
.run(input.name, input.description, input.status, autoApprove, reviewDeliverables, ts, id);
} else {
this.db
.prepare("insert into companies (id, name, description, status, auto_approve_hires, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?)")
.run(id, input.name, input.description, input.status, autoApprove, ts, ts);
.prepare("insert into companies (id, name, description, status, auto_approve_hires, review_deliverables, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?)")
.run(id, input.name, input.description, input.status, autoApprove, reviewDeliverables, ts, ts);
if (!this.getCurrentCompanyId()) {
this.setCurrentCompany(id);
}
@@ -1098,10 +1099,11 @@ export class AppDatabase {
getTask(id: string): TaskRecord {
const row = this.db
.prepare(
"select id, company_id as companyId, project_id as projectId, goal_id as goalId, parent_id as parentId, title, description, assignee_agent_id as assigneeAgentId, workspace_id as workspaceId, status, priority, active_run_id as activeRunId, session_json as sessionJson, session_display_id as sessionDisplayId, cost_usd as costUsd, created_at as createdAt, updated_at as updatedAt from tasks where id = ?",
"select id, company_id as companyId, project_id as projectId, goal_id as goalId, parent_id as parentId, title, description, assignee_agent_id as assigneeAgentId, workspace_id as workspaceId, status, priority, active_run_id as activeRunId, session_json as sessionJson, session_display_id as sessionDisplayId, cost_usd as costUsd, requires_user_review as requiresUserReview, created_at as createdAt, updated_at as updatedAt from tasks where id = ?",
)
.get(id) as TaskRecord | undefined;
if (!row) throw new Error(`Task not found: ${id}`);
row.requiresUserReview = Boolean(row.requiresUserReview);
return row;
}
@@ -2180,7 +2182,9 @@ export class AppDatabase {
addColumnIfMissing("alter table agents add column heartbeat_interval_sec integer not null default 120");
addColumnIfMissing("alter table agents add column last_heartbeat_at text");
addColumnIfMissing("alter table companies add column auto_approve_hires integer not null default 0");
addColumnIfMissing("alter table companies add column review_deliverables integer not null default 0");
addColumnIfMissing("alter table tasks add column task_type text not null default 'general'");
addColumnIfMissing("alter table tasks add column requires_user_review integer not null default 0");
addColumnIfMissing("alter table tasks add column retry_count integer not null default 0");
addColumnIfMissing("alter table tasks add column last_failure_at text");
this.db.exec(`
@@ -2275,6 +2279,7 @@ export class AppDatabase {
or (type = 'dangerous_command' and ${taskCondition})
or (type = 'secret_access' and ${taskCondition})
or (type = 'approve_ceo_strategy' and ${taskCondition})
or (type = 'deliverable_review' and (requested_by_agent_id = ? or (related_task_id is not null and ${taskCondition})))
)
order by created_at asc limit 1`,
companyId,
@@ -2282,6 +2287,8 @@ export class AppDatabase {
...taskParams,
...taskParams,
...taskParams,
agentId,
...taskParams,
);
return rows.length > 0 ? rows[0] : null;
}
@@ -2802,6 +2809,43 @@ export class AppDatabase {
return { previousStatus };
}
setTaskRequiresUserReview(taskId: string, value: boolean) {
const ts = nowIso();
this.db.prepare("update tasks set requires_user_review = ?, updated_at = ? where id = ?").run(value ? 1 : 0, ts, taskId);
}
/** Returns true if the company has any pending deliverable_review approvals or tasks awaiting review. */
hasPendingDeliverableReviews(companyId: string): boolean {
const approval = this.db.prepare(
"select 1 from approvals where company_id = ? and type = 'deliverable_review' and state = 'pending' limit 1",
).get(companyId);
if (approval) return true;
const flaggedTask = this.db.prepare(
"select 1 from tasks where company_id = ? and requires_user_review = 1 limit 1",
).get(companyId);
return !!flaggedTask;
}
/** Returns true if this specific agent has tasks with pending deliverable_review approvals. */
hasAgentPendingDeliverableReview(agentId: string, companyId: string): boolean {
const row = this.db.prepare(
`select 1 from approvals a
join tasks t on a.related_task_id = t.id
where a.company_id = ? and a.type = 'deliverable_review' and a.state = 'pending'
and t.assignee_agent_id = ?
limit 1`,
).get(companyId, agentId);
return !!row;
}
/** Returns true if agent has any tasks flagged requires_user_review (before approval is created). */
hasAgentTaskAwaitingReview(agentId: string, companyId: string): boolean {
const row = this.db.prepare(
"select 1 from tasks where assignee_agent_id = ? and company_id = ? and requires_user_review = 1 limit 1",
).get(agentId, companyId);
return !!row;
}
private incrementActiveSprintVelocity(companyId: string, delta: number) {
const ts = nowIso();
const today = ts.slice(0, 10);
+4
View File
@@ -346,6 +346,10 @@ export function registerCoreWorkGraphHandlers({
if (previousStatus && previousStatus !== parsed.status) {
handleTaskStatusChange(id, parsed.companyId, previousStatus, parsed.status);
}
// User (board) manually advancing a flagged task to done: clear the review flag
if (parsed.status === "done") {
try { db.setTaskRequiresUserReview(id, false); } catch { /* best effort */ }
}
// Wake new assignee when task is reassigned (matches api-server.ts behavior)
if (parsed.assigneeAgentId && parsed.assigneeAgentId !== previousAssigneeAgentId) {
wakeAgentIfPossible(parsed.assigneeAgentId, parsed.companyId, "assignment");
+2
View File
@@ -17,6 +17,7 @@ interface BootstrapInput {
companyDescription: string;
workspacePath: string;
autoApproveHires: boolean;
reviewDeliverables?: boolean;
goalTitle: string;
goalDescription: string;
connectorId?: ConnectorId;
@@ -188,6 +189,7 @@ export function createOnboardingService({
description: input.companyDescription.trim(),
status: "active",
autoApproveHires: input.autoApproveHires,
reviewDeliverables: input.reviewDeliverables,
});
const workspaceId = createDefaultWorkspace(companyId, input.workspacePath);
+14
View File
@@ -335,6 +335,20 @@ export function createOrchestrationService({
return null;
}
// Block heartbeat when agent has tasks awaiting user deliverable review
// Check both: pending approval (gate created) and requires_user_review flag (gap before approval)
if (db.hasAgentPendingDeliverableReview(agentId, companyId) || db.hasAgentTaskAwaitingReview(agentId, companyId)) {
logger.info(`[heartbeat] Skipping ${agent.name} — awaiting user deliverable review.`);
return null;
}
// Block CEO heartbeat when company has any pending deliverable reviews
// CEO should not create new work while the user hasn't reviewed existing deliverables
if (!agent.reportsTo && db.hasPendingDeliverableReviews(companyId)) {
logger.info(`[heartbeat] Skipping CEO ${agent.name} — company has pending deliverable reviews.`);
return null;
}
const assignedTasks = db.listAgentTasks(agentId, companyId, ["todo", "in_progress", "blocked"]);
const isTimer = trigger === "timer";
const candidateTasks = isTimer
+4
View File
@@ -1,3 +1,4 @@
import { app } from "electron";
import type { AgentApiServer } from "./api-server";
import type { AppDatabase } from "./database";
import { BrowserManager } from "./browser-manager";
@@ -225,6 +226,9 @@ export function createServiceGraph({
updaterService = null;
appLifecycleService = null;
db.close();
// Force exit after grace period — covers all platforms (macOS Dock, Windows tray, Linux)
// Worker child processes (claude/codex CLI) may keep the event loop alive indefinitely
setTimeout(() => { app.exit(0); }, 3000);
},
});
}
+9 -2
View File
@@ -426,8 +426,15 @@ export function createWorkerService({
}
function destroy() {
worker?.kill();
worker = null;
if (worker) {
worker.kill();
// Force-kill after 2s if SIGTERM didn't work (child CLI processes may keep it alive)
const ref = worker;
setTimeout(() => {
try { ref.kill(); } catch { /* already dead */ }
}, 2000);
worker = null;
}
}
return {
+1
View File
@@ -1087,6 +1087,7 @@ export function App() {
approval={selectedApproval}
agents={companyAgents}
tasks={tasks}
documents={companyDocuments}
companyId={currentCompany!.id}
onDecide={async (state, note) => {
unwrap(await window.agentCompany.decideApproval({ companyId: currentCompany!.id, approvalId: selectedApproval.id, state, decisionNote: note ?? `${state} from desktop control surface.` }));
@@ -1,5 +1,5 @@
import { motion } from "framer-motion";
import { Buildings, FolderOpen } from "@phosphor-icons/react";
import { Buildings, FolderOpen, Lightning, UserCircleCheck } from "@phosphor-icons/react";
import type { ProfileSnapshot } from "@shared/types";
import type { AppLocale } from "@shared/locale";
import { ActionButton, Input, Select, TextArea } from "../ui";
@@ -16,6 +16,8 @@ interface CompanyStepProps {
setWorkspacePath: (v: string) => void;
autoApproveHires: boolean;
setAutoApproveHires: (v: boolean) => void;
reviewDeliverables: boolean;
setReviewDeliverables: (v: boolean) => void;
onPickDirectory: () => void;
onContinue: () => void;
onBack: () => void;
@@ -38,6 +40,8 @@ export function CompanyStep({
setWorkspacePath,
autoApproveHires,
setAutoApproveHires,
reviewDeliverables,
setReviewDeliverables,
onPickDirectory,
onContinue,
onBack,
@@ -111,21 +115,28 @@ export function CompanyStep({
{t("onboarding.workspaceHelp")}
</div>
</div>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={autoApproveHires}
onChange={(event) => setAutoApproveHires(event.target.checked)}
className="h-4 w-4 rounded border-[color:var(--line)] accent-[color:var(--accent)]"
/>
<div>
<div className="text-[13px] font-medium text-[color:var(--text)]">{t("onboarding.autoApproveLabel")}</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
{t("onboarding.autoApproveHelp")}
</div>
</div>
</label>
<div>
<div className="mb-2 text-[12px] font-medium text-[color:var(--muted-strong)]">{t("onboarding.automationMode")}</div>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => { setReviewDeliverables(false); setAutoApproveHires(true) }}
className={`cursor-pointer rounded-[8px] border-2 px-4 py-3.5 text-left transition ${!reviewDeliverables ? "border-[color:var(--accent)] bg-[color:var(--accent-soft)]" : "border-[color:var(--line)] bg-[color:var(--panel-soft)] hover:border-[color:var(--muted)]"}`}
>
<Lightning size={20} weight={!reviewDeliverables ? "fill" : "regular"} className={!reviewDeliverables ? "text-[color:var(--accent)]" : "text-[color:var(--muted)]"} />
<div className="mt-2 text-[13px] font-semibold text-[color:var(--text)]">{t("onboarding.modeFullAuto")}</div>
<div className="mt-1 text-[11px] leading-relaxed text-[color:var(--muted)]">{t("onboarding.modeFullAutoDesc")}</div>
</button>
<button
type="button"
onClick={() => { setReviewDeliverables(true); setAutoApproveHires(false) }}
className={`cursor-pointer rounded-[8px] border-2 px-4 py-3.5 text-left transition ${reviewDeliverables ? "border-[color:var(--accent)] bg-[color:var(--accent-soft)]" : "border-[color:var(--line)] bg-[color:var(--panel-soft)] hover:border-[color:var(--muted)]"}`}
>
<UserCircleCheck size={20} weight={reviewDeliverables ? "fill" : "regular"} className={reviewDeliverables ? "text-[color:var(--accent)]" : "text-[color:var(--muted)]"} />
<div className="mt-2 text-[13px] font-semibold text-[color:var(--text)]">{t("onboarding.modeHumanReview")}</div>
<div className="mt-1 text-[11px] leading-relaxed text-[color:var(--muted)]">{t("onboarding.modeHumanReviewDesc")}</div>
</button>
</div>
</div>
</div>
@@ -37,6 +37,7 @@ export function CompanyOnboarding({
const [companyDescription, setCompanyDescription] = useState("");
const [workspacePath, setWorkspacePath] = useState("");
const [autoApproveHires, setAutoApproveHires] = useState(true);
const [reviewDeliverables, setReviewDeliverables] = useState(false);
const [goalTitle, setGoalTitle] = useState("");
const [goalDescription, setGoalDescription] = useState("");
const [loading, setLoading] = useState(false);
@@ -123,6 +124,7 @@ export function CompanyOnboarding({
companyDescription: companyDescription.trim(),
workspacePath: workspacePath.trim(),
autoApproveHires,
reviewDeliverables,
goalTitle: goalTitle.trim(),
goalDescription: goalDescription.trim(),
connectorId: selectedConnector?.id,
@@ -131,6 +133,7 @@ export function CompanyOnboarding({
setStep("launch");
}, [
autoApproveHires,
reviewDeliverables,
companyDescription,
companyName,
goalDescription,
@@ -231,6 +234,8 @@ export function CompanyOnboarding({
setWorkspacePath={setWorkspacePath}
autoApproveHires={autoApproveHires}
setAutoApproveHires={setAutoApproveHires}
reviewDeliverables={reviewDeliverables}
setReviewDeliverables={setReviewDeliverables}
onPickDirectory={() => void pickDirectory()}
onContinue={() => void continueToMission()}
onBack={() => setStep("language")}
@@ -1,6 +1,7 @@
import { useState } from "react"
import { Briefcase, CheckCircle, Clock, ClockCounterClockwise, Robot, Trash, XCircle } from "@phosphor-icons/react"
import type { AgentRecord, ApprovalRecord, TaskRecord } from "@shared/types"
import { CaretDown, CaretUp } from "@phosphor-icons/react"
import type { AgentRecord, ApprovalRecord, DocumentRecord, TaskRecord } from "@shared/types"
import { useT } from "../../i18n"
import { formatMoney, formatTime } from "../../lib/formatters"
import { unwrap } from "../../lib/desktop"
@@ -14,11 +15,12 @@ function getTypeBadgeClass(type: string): string {
if (type === "approve_ceo_strategy") return "bg-[color:var(--warn-soft)] text-[color:var(--warn)]"
if (type === "secret_access") return "bg-[color:var(--danger-soft)] text-[color:var(--danger)]"
if (type === "social_post") return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
if (type === "deliverable_review") return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
return "bg-[color:var(--panel-soft)] text-[color:var(--muted)]"
}
export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide, onDeleted }: {
approval: ApprovalRecord; agents: AgentRecord[]; tasks: TaskRecord[]; companyId: string
export function ApprovalInspector({ approval, agents, tasks, documents, companyId, onDecide, onDeleted }: {
approval: ApprovalRecord; agents: AgentRecord[]; tasks: TaskRecord[]; documents?: DocumentRecord[]; companyId: string
onDecide: (state: "approved" | "rejected" | "revision_requested", note?: string) => Promise<void>; onDeleted: () => Promise<void>
}) {
const t = useT()
@@ -112,6 +114,7 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
{mgr ? <div><span className="text-[color:var(--muted)]">Reports to:</span> <span className="text-[color:var(--text)]">{mgr.name}</span></div> : null}
</div></div>) })() : null}
{approval.type === "approve_ceo_strategy" ? <div className="rounded-[8px] border border-[color:var(--warn)] border-opacity-30 bg-[color:var(--warn-soft)] px-4 py-3"><div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">Organization Strategy</div><div className="whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--text)]">{approval.payloadSummary}</div></div> : null}
{approval.type === "deliverable_review" ? <DeliverableReviewPanel approval={approval} documents={documents} agents={agents} /> : null}
{isSocialPost && approval.payloadJson ? (
<SocialDraftPreview
payloadJson={approval.payloadJson}
@@ -187,3 +190,46 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide
</div>
)
}
function DeliverableReviewPanel({ approval, documents, agents }: { approval: ApprovalRecord; documents?: DocumentRecord[]; agents: AgentRecord[] }) {
const payload = parseApprovalPayloadJson(approval)
const docRefs = Array.isArray(payload?.documents) ? payload.documents as Array<{ id: string; type: string; title: string }> : []
const docIds = Array.isArray(payload?.documentIds) ? payload.documentIds as string[] : docRefs.map((d) => d.id)
const fullDocs = documents ? docIds.map((id) => documents.find((d) => d.id === id)).filter(Boolean) as DocumentRecord[] : []
const [expandedDocId, setExpandedDocId] = useState<string | null>(fullDocs.length === 1 ? fullDocs[0]!.id : null)
return (
<div className="space-y-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--accent)]">Deliverable Review</div>
{fullDocs.length > 0 ? fullDocs.map((doc) => {
const isExpanded = expandedDocId === doc.id
const author = doc.authorAgentId ? agents.find((a) => a.id === doc.authorAgentId) : null
return (
<div key={doc.id} className="rounded-[8px] border border-[color:var(--line)] overflow-hidden">
<button
type="button"
onClick={() => setExpandedDocId(isExpanded ? null : doc.id)}
className="flex w-full cursor-pointer items-center gap-3 bg-[color:var(--panel-soft)] px-4 py-3 text-left transition hover:bg-[color:var(--panel)]"
>
<span className="shrink-0 rounded-full bg-[color:var(--accent-soft)] px-2 py-0.5 text-[10px] font-semibold uppercase text-[color:var(--accent)]">{doc.type.replaceAll("_", " ")}</span>
<span className="min-w-0 flex-1 truncate text-[13px] font-medium text-[color:var(--text)]">{doc.title}</span>
{isExpanded ? <CaretUp size={14} className="shrink-0 text-[color:var(--muted)]" /> : <CaretDown size={14} className="shrink-0 text-[color:var(--muted)]" />}
</button>
{isExpanded ? (
<div className="border-t border-[color:var(--line)] px-4 py-3">
{author ? <div className="mb-2 text-[11px] text-[color:var(--muted)]">Author: <span className="font-medium text-[color:var(--text)]">{author.name}</span> ({author.role})</div> : null}
<div className="whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--text)]">{doc.content || "No content."}</div>
</div>
) : null}
</div>
)
}) : docRefs.length > 0 ? docRefs.map((doc) => (
<div key={doc.id} className="flex items-center gap-2 rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px]">
<span className="rounded-full bg-[color:var(--accent-soft)] px-2 py-0.5 text-[10px] font-semibold uppercase text-[color:var(--accent)]">{doc.type.replaceAll("_", " ")}</span>
<span className="font-medium text-[color:var(--text)]">{doc.title}</span>
</div>
)) : <div className="text-[12px] text-[color:var(--muted)]">{approval.payloadSummary}</div>}
{approval.impactSummary ? <div className="text-[11px] text-[color:var(--muted)]">{approval.impactSummary}</div> : null}
</div>
)
}
@@ -37,7 +37,10 @@ export function SettingsPane({ snapshot, secrets, companyId, onSaved }: { snapsh
</div>
{(() => { const company = snapshot.companies.find((c) => c.id === companyId); if (!company) return null; return (
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.companyAutonomy")}</div>
<div className="rounded-[8px] border border-[color:var(--line)] px-4 py-3"><label className="flex items-center gap-3"><input type="checkbox" checked={company.autoApproveHires} onChange={async (e) => { setActionError(null); try { unwrap(await window.agentCompany.saveCompany({ id: company.id, name: company.name, description: company.description, status: company.status, autoApproveHires: e.target.checked })); await onSaved() } catch (err) { setActionError(describeActionError(err)) } }} className="h-4 w-4 rounded accent-[color:var(--accent)]" /><div><div className="text-[13px] font-medium text-[color:var(--text)]">{t("exec.autoApproveLabel")}</div><div className="mt-0.5 text-[11px] text-[color:var(--muted)]">{t("exec.autoApproveDesc")}</div></div></label></div></div>) })()}
<div className="space-y-3">
<div className="rounded-[8px] border border-[color:var(--line)] px-4 py-3"><label className="flex items-center gap-3"><input type="checkbox" checked={company.autoApproveHires} onChange={async (e) => { setActionError(null); try { unwrap(await window.agentCompany.saveCompany({ id: company.id, name: company.name, description: company.description, status: company.status, autoApproveHires: e.target.checked, reviewDeliverables: company.reviewDeliverables })); await onSaved() } catch (err) { setActionError(describeActionError(err)) } }} className="h-4 w-4 rounded accent-[color:var(--accent)]" /><div><div className="text-[13px] font-medium text-[color:var(--text)]">{t("exec.autoApproveLabel")}</div><div className="mt-0.5 text-[11px] text-[color:var(--muted)]">{t("exec.autoApproveDesc")}</div></div></label></div>
<div className="rounded-[8px] border border-[color:var(--line)] px-4 py-3"><label className="flex items-center gap-3"><input type="checkbox" checked={!company.reviewDeliverables} onChange={async (e) => { setActionError(null); try { unwrap(await window.agentCompany.saveCompany({ id: company.id, name: company.name, description: company.description, status: company.status, autoApproveHires: company.autoApproveHires, reviewDeliverables: !e.target.checked })); await onSaved() } catch (err) { setActionError(describeActionError(err)) } }} className="h-4 w-4 rounded accent-[color:var(--accent)]" /><div><div className="text-[13px] font-medium text-[color:var(--text)]">{t("exec.reviewDeliverablesLabel")}</div><div className="mt-0.5 text-[11px] text-[color:var(--muted)]">{t("exec.reviewDeliverablesDesc")}</div></div></label></div>
</div></div>) })()}
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.actions")}</div><div className="grid gap-3 md:grid-cols-2"><ActionButton label={t("exec.saveSettings")} tone="accent" onClick={() => void handleSaveSettings()} icon={CheckCircle} loading={loadingAction === "save"} /><ActionButton label={t("exec.checkForUpdates")} onClick={() => void handleCheckForUpdates()} icon={Download} loading={loadingAction === "check"} /><ActionButton label={t("exec.backupProfile")} onClick={() => void handleBackupProfile()} icon={ClockCounterClockwise} loading={loadingAction === "backup"} /><ActionButton label={t("exec.restoreProfile")} onClick={() => void handleRestoreProfile()} icon={ArrowClockwise} loading={loadingAction === "restore"} /></div></div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretBackend")}</div><div className="border-y border-[color:var(--line)] py-3 text-[13px] text-[color:var(--muted-strong)]">{snapshot.backend.secretBackend}</div></div>
<div><div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secrets")} ({secrets.length})</div>{secrets.length === 0 ? <EmptyState title={t("exec.noSecretsStored")} /> : <div className="border-y border-[color:var(--line)] overflow-x-auto"><table className="w-full text-left text-[13px]"><thead><tr className="border-b border-[color:var(--line)]"><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretName")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretDescription")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretBackendCol")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("exec.secretUpdated")}</th><th className="px-4 py-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]" /></tr></thead><tbody>{secrets.map((s) => <tr key={s.id} className="border-b border-[color:var(--line)] last:border-b-0"><td className="px-4 py-3 font-medium text-[color:var(--text)]">{s.name}</td><td className="px-4 py-3 text-[color:var(--muted)]">{s.description || "\u2014"}</td><td className="px-4 py-3 mono text-[color:var(--muted-strong)]">{s.backend}</td><td className="px-4 py-3 mono text-[color:var(--muted)]">{formatTime(s.updatedAt)}</td><td className="px-4 py-3"><ActionButton label="Delete" onClick={() => setConfirmDeleteSecretId(s.id)} icon={Trash} tone="danger" /></td></tr>)}</tbody></table></div>}</div>
+9
View File
@@ -73,6 +73,13 @@ export const en: Record<string, string> = {
"onboarding.autoApprove": "Auto-approve hire and strategy approvals",
"onboarding.autoApproveLabel": "Auto-approve hire and strategy approvals",
"onboarding.autoApproveHelp": "Skips board review for hiring and strategy changes.",
"onboarding.reviewDeliverablesLabel": "Review important deliverables",
"onboarding.reviewDeliverablesHelp": "Pause workflow for your review on PRDs, design docs, and key documents.",
"onboarding.automationMode": "Automation mode",
"onboarding.modeFullAuto": "Full automation",
"onboarding.modeFullAutoDesc": "Agents run end-to-end without pausing. Best for hands-off operation.",
"onboarding.modeHumanReview": "Human in the loop",
"onboarding.modeHumanReviewDesc": "Pause for your review on PRDs, design docs, and other key deliverables.",
"onboarding.continue": "Continue",
"onboarding.back": "Back",
"onboarding.goal": "Goal",
@@ -426,6 +433,8 @@ export const en: Record<string, string> = {
"exec.companyAutonomy": "Company autonomy",
"exec.autoApproveLabel": "Auto-approve hire and strategy approvals",
"exec.autoApproveDesc": "Auto-approve hiring and CEO strategy decisions without board review.",
"exec.reviewDeliverablesLabel": "Auto-approve important deliverables",
"exec.reviewDeliverablesDesc": "Skip board review when agents produce PRDs, design docs, and other key documents.",
"exec.actions": "Actions",
"exec.saveSettings": "Save settings",
"exec.checkForUpdates": "Check for updates",
+9
View File
@@ -73,6 +73,13 @@ export const zh: Record<string, string> = {
"onboarding.autoApprove": "自动批准招聘和战略审批",
"onboarding.autoApproveLabel": "自动批准招聘和战略审批",
"onboarding.autoApproveHelp": "跳过招聘和战略变更的董事会审批。",
"onboarding.reviewDeliverablesLabel": "重要产出物人工审核",
"onboarding.reviewDeliverablesHelp": "当 Agent 产出 PRD、设计文档等重要产出物时暂停流程,等待您审核。",
"onboarding.automationMode": "自动化模式",
"onboarding.modeFullAuto": "全自动",
"onboarding.modeFullAutoDesc": "Agent 全程自动运行,无需人工干预。适合放手运营。",
"onboarding.modeHumanReview": "人工审核",
"onboarding.modeHumanReviewDesc": "当产出 PRD、设计文档等重要产出物时暂停,等待您审核后继续。",
"onboarding.continue": "继续",
"onboarding.back": "返回",
"onboarding.goal": "目标",
@@ -426,6 +433,8 @@ export const zh: Record<string, string> = {
"exec.companyAutonomy": "公司自治",
"exec.autoApproveLabel": "自动批准招聘和战略审批",
"exec.autoApproveDesc": "自动批准招聘和 CEO 战略决策,无需董事会审批。",
"exec.reviewDeliverablesLabel": "自动通过重要产出物",
"exec.reviewDeliverablesDesc": "自动通过 Agent 产出的 PRD、设计文档等重要产出物,无需人工审核。",
"exec.actions": "操作",
"exec.saveSettings": "保存设置",
"exec.checkForUpdates": "检查更新",
+3 -1
View File
@@ -86,6 +86,7 @@ export const companyInputSchema = z.object({
description: z.string().max(500).default(""),
status: z.enum(["active", "paused", "archived"]).default("active"),
autoApproveHires: z.boolean().default(false),
reviewDeliverables: z.boolean().default(false),
});
export const connectorInputSchema = z.object({
@@ -180,7 +181,7 @@ export const approvalInputSchema = z.object({
relatedTaskId: optionalId,
requestedByAgentId: optionalId,
relatedAgentId: optionalId,
type: z.enum(["hire_agent", "approve_ceo_strategy", "dangerous_command", "secret_access", "budget_request", "promotion", "termination", "vendor_approval", "document_review", "production_deploy", "incident_escalation", "social_post"]),
type: z.enum(["hire_agent", "approve_ceo_strategy", "dangerous_command", "secret_access", "budget_request", "promotion", "termination", "vendor_approval", "document_review", "production_deploy", "incident_escalation", "social_post", "deliverable_review"]),
payloadSummary: z.string().min(2).max(280),
impactSummary: z.string().min(2).max(400),
payloadJson: z.string().nullable().optional(),
@@ -287,6 +288,7 @@ export const bootstrapOnboardingSchema = z.object({
companyDescription: z.string().max(500).default(""),
workspacePath: z.string().default(""),
autoApproveHires: z.boolean().default(true),
reviewDeliverables: z.boolean().default(false),
goalTitle: z.string().default(""),
goalDescription: z.string().default(""),
connectorId: z.enum(["codex_local", "claude_local", "gemini_local"]).optional(),
+4 -1
View File
@@ -65,7 +65,8 @@ export type ApprovalType =
| "document_review"
| "production_deploy"
| "incident_escalation"
| "social_post";
| "social_post"
| "deliverable_review";
export type RunStatus =
| "queued"
| "running"
@@ -89,6 +90,7 @@ export interface CompanyRecord {
description: string;
status: CompanyStatus;
autoApproveHires: boolean;
reviewDeliverables: boolean;
createdAt: string;
updatedAt: string;
}
@@ -202,6 +204,7 @@ export interface TaskRecord {
sessionJson: string | null;
sessionDisplayId: string | null;
costUsd: number;
requiresUserReview: boolean;
createdAt: string;
updatedAt: string;
}