109 KiB
109 KiB
GooseFactory — Interface Contracts
Version: 1.0.0
Last Updated: 2025-07-14
Author: Architect Agent
Status: CANONICAL — All agents build to these types.
Every boundary between agents is defined here. If it's not in this document, it's not a contract. If two agents disagree, this document wins.
Table of Contents
- API Contract (REST + WebSocket)
- MCP Tool Schemas
- Modal Data Contract
- UI Component Contracts
- Rust Backend Contracts
- Learning System Contracts
- Cross-Cutting Contracts
1. API Contract
1.1 Base Configuration
// Base URL
const API_BASE = "https://api.goosefactory.dev/v1";
const WS_BASE = "wss://api.goosefactory.dev/v1/ws";
// All requests require:
// Authorization: Bearer <jwt>
// Content-Type: application/json
// X-Request-Id: <uuid> (optional, for tracing)
// All responses include:
// X-Request-Id: <uuid>
// X-API-Version: "1"
1.2 Authentication Types
// === JWT Payload ===
interface JWTPayload {
sub: string; // user ID (UUID)
email: string;
role: Role;
scopes: Scope[];
iat: number; // issued at (unix)
exp: number; // expires (unix, +15min)
}
type Role = "owner" | "admin" | "operator" | "viewer" | "agent";
type Scope =
| "pipelines:read" | "pipelines:write"
| "tasks:read" | "tasks:approve" | "tasks:reject"
| "deploy:staging" | "deploy:production"
| "agents:manage"
| "assets:read" | "assets:write"
| "audit:read"
| "feedback:read" | "feedback:write"
| "learning:read" | "learning:write";
// === API Key (server-to-server) ===
// Header: X-API-Key: <key>
// Keys are scoped to specific Scope[] subsets
// Keys are rotatable via POST /v1/auth/keys/rotate
// === Auth Endpoints ===
// POST /v1/auth/login → { access_token, refresh_token, expires_in }
// POST /v1/auth/refresh → { access_token, expires_in }
// POST /v1/auth/logout → 204
// POST /v1/auth/keys → { api_key, key_id, scopes }
// DELETE /v1/auth/keys/:id → 204
1.3 Standard Response Envelope
// === Success Response ===
interface ApiResponse<T> {
ok: true;
data: T;
meta?: ResponseMeta;
}
interface ResponseMeta {
requestId: string;
timestamp: string; // ISO 8601
pagination?: PaginationMeta;
rateLimit?: RateLimitMeta;
}
interface PaginationMeta {
page: number;
limit: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
}
interface RateLimitMeta {
limit: number;
remaining: number;
resetAt: string; // ISO 8601
}
// === Error Response ===
interface ApiError {
ok: false;
error: {
code: ErrorCode;
message: string; // Human-readable
details?: Record<string, unknown>; // Machine-readable context
field?: string; // For validation errors
requestId: string;
};
}
type ErrorCode =
// 4xx Client Errors
| "BAD_REQUEST" // 400 — malformed input
| "VALIDATION_ERROR" // 400 — schema validation failed
| "UNAUTHORIZED" // 401 — missing/invalid token
| "FORBIDDEN" // 403 — insufficient scopes
| "NOT_FOUND" // 404 — entity doesn't exist
| "CONFLICT" // 409 — state conflict (e.g., already approved)
| "GONE" // 410 — entity archived/deleted
| "RATE_LIMITED" // 429 — too many requests
// 5xx Server Errors
| "INTERNAL_ERROR" // 500 — unexpected failure
| "SERVICE_UNAVAILABLE" // 503 — dependency down
| "TIMEOUT" // 504 — upstream timeout
;
// === Pagination Request ===
interface PaginationParams {
page?: number; // default: 1
limit?: number; // default: 20, max: 100
sort?: string; // field name, prefix with - for desc: "-created_at"
}
1.4 Core Entity Types
// ═══════════════════════════════════════
// PIPELINE
// ═══════════════════════════════════════
interface Pipeline {
id: string; // UUID
name: string;
slug: string; // URL-safe unique identifier
template: PipelineTemplate;
platform: string; // e.g., "go-high-level", "shopify"
// State
currentStage: PipelineStage;
status: PipelineStatus;
priority: Priority;
// Ownership
createdBy: string; // user ID
assigneeId: string | null; // user ID
// Config
config: Record<string, unknown>;
metadata: Record<string, unknown>;
// SLA
slaDeadline: string | null; // ISO 8601
startedAt: string;
completedAt: string | null;
// Timestamps
createdAt: string;
updatedAt: string;
}
type PipelineStatus = "active" | "paused" | "completed" | "failed" | "archived";
type Priority = "critical" | "high" | "medium" | "low";
type PipelineTemplate = "mcp-server-standard" | "mcp-server-minimal" | "mcp-server-enterprise";
type PipelineStage =
| "intake"
| "scaffolding"
| "building"
| "testing"
| "review" // ★ HUMAN GATE
| "staging" // ★ HUMAN GATE
| "production" // ★ HUMAN GATE
| "published";
// ═══════════════════════════════════════
// PIPELINE STAGE RECORD
// ═══════════════════════════════════════
interface PipelineStageRecord {
id: string;
pipelineId: string;
stageName: PipelineStage;
stageOrder: number;
status: StageStatus;
requiresApproval: boolean;
approvalType: "manual" | "auto" | "conditional";
autoAdvance: boolean;
validationRules: ValidationRule[];
enteredAt: string | null;
completedAt: string | null;
durationSeconds: number | null;
createdAt: string;
}
type StageStatus = "pending" | "active" | "completed" | "skipped" | "failed";
interface ValidationRule {
type: "test_coverage" | "tests_passing" | "required_asset" | "approval_count" | "custom";
config: Record<string, unknown>;
// e.g., { type: "test_coverage", config: { minPercent: 80 } }
// e.g., { type: "required_asset", config: { assetType: "readme" } }
}
// ═══════════════════════════════════════
// TASK (Decision Queue Item)
// ═══════════════════════════════════════
interface Task {
id: string; // UUID
pipelineId: string | null;
stageName: PipelineStage | null;
// Details
type: TaskType;
title: string;
description: string | null;
context: TaskContext;
// Queue
status: TaskStatus;
priority: Priority;
// Assignment
assigneeId: string | null;
claimedAt: string | null;
claimedBy: string | null;
// Decision
decision: TaskDecision | null;
decisionNotes: string | null;
decisionData: Record<string, unknown>;
decidedAt: string | null;
decidedBy: string | null;
// SLA
slaDeadline: string | null;
slaWarningsSent: number;
slaBreached: boolean;
escalationLevel: number; // 0 = none, 1-4 = escalation tiers
// Blocking
blocksStageAdvance: boolean;
blocksPipelineId: string | null;
// Timestamps
createdAt: string;
updatedAt: string;
}
type TaskType =
| "approval" // Gate approval (stage transition)
| "review" // Code/design/doc review
| "decision" // Choose between options
| "manual_action" // Something only a human can do
| "fix_required"; // Something broke, needs intervention
type TaskStatus =
| "pending" // Waiting for someone to act
| "claimed" // Someone is looking at it
| "in_progress" // Active work happening
| "completed" // Decision made
| "expired" // SLA breached, auto-escalated
| "escalated"; // Bumped to higher authority
type TaskDecision = "approved" | "rejected" | "deferred" | "escalated";
interface TaskContext {
// What's being decided
summary: string;
details?: string;
// Supporting data
testResults?: TestResults;
diffSummary?: DiffSummary;
metrics?: Record<string, number>;
relatedAssets?: string[]; // asset IDs
// For modals
modalType?: string; // Which HITL modal to show
modalConfig?: Record<string, unknown>;
}
interface TestResults {
total: number;
passed: number;
failed: number;
skipped: number;
coveragePercent: number;
failureDetails?: TestFailure[];
}
interface TestFailure {
testName: string;
error: string;
file?: string;
line?: number;
}
interface DiffSummary {
filesChanged: number;
insertions: number;
deletions: number;
files: DiffFile[];
}
interface DiffFile {
path: string;
status: "added" | "modified" | "deleted" | "renamed";
insertions: number;
deletions: number;
patch?: string; // Unified diff format
}
// ═══════════════════════════════════════
// APPROVAL
// ═══════════════════════════════════════
interface Approval {
id: string;
taskId: string;
pipelineId: string;
stageName: PipelineStage;
type: ApprovalType;
status: ApprovalStatus;
approvedBy: string | null;
approvedAt: string | null;
rejectionReason: string | null;
conditions: string[];
requiredApprovers: number;
currentApprovers: number;
createdAt: string;
}
type ApprovalType = "stage_gate" | "deploy" | "code_review" | "manual_check";
type ApprovalStatus = "pending" | "approved" | "rejected" | "expired";
// ═══════════════════════════════════════
// AGENT
// ═══════════════════════════════════════
interface Agent {
id: string;
name: string;
type: AgentType;
status: AgentStatus;
currentTaskId: string | null;
currentPipelineId: string | null;
health: AgentHealth;
capabilities: string[];
config: Record<string, unknown>;
tasksCompletedTotal: number;
avgTaskDurationSeconds: number | null;
createdAt: string;
updatedAt: string;
}
type AgentType = "ai_builder" | "test_runner" | "deployer" | "monitor";
type AgentStatus = "idle" | "active" | "error" | "offline";
interface AgentHealth {
uptimeSeconds: number;
tasksCompleted24h: number;
errorRate: number; // 0.0 - 1.0
lastHeartbeat: string; // ISO 8601
}
// ═══════════════════════════════════════
// ASSET
// ═══════════════════════════════════════
interface Asset {
id: string;
pipelineId: string;
stageName: PipelineStage | null;
type: AssetType;
name: string;
path: string | null;
storageKey: string;
storageBucket: string | null;
sizeBytes: number;
contentType: string;
checksumSha256: string;
version: number;
previousVersionId: string | null;
generatedBy: string; // user or agent ID
generatorType: "user" | "agent" | "ci";
metadata: Record<string, unknown>;
createdAt: string;
}
type AssetType = "code" | "config" | "docs" | "build" | "test_report" | "screenshot";
// ═══════════════════════════════════════
// NOTIFICATION
// ═══════════════════════════════════════
interface Notification {
id: string;
userId: string;
type: NotificationType;
title: string;
body: string | null;
data: Record<string, unknown>;
channels: NotificationChannel[];
deliveredVia: NotificationChannel[];
read: boolean;
readAt: string | null;
dismissed: boolean;
entityType: string | null;
entityId: string | null;
actionUrl: string | null;
createdAt: string;
}
type NotificationType =
| "task_assigned"
| "approval_pending"
| "sla_warning"
| "sla_breach"
| "deploy_status"
| "pipeline_completed"
| "agent_error"
| "feedback_processed";
type NotificationChannel = "dashboard" | "discord" | "email" | "sms" | "push";
// ═══════════════════════════════════════
// AUDIT LOG ENTRY
// ═══════════════════════════════════════
interface AuditEntry {
id: string;
actorType: "user" | "agent" | "system" | "webhook";
actorId: string | null;
actorName: string | null;
action: string; // e.g., "pipeline.created", "task.approved"
entityType: string; // e.g., "pipeline", "task", "approval"
entityId: string;
changes: Record<string, { from: unknown; to: unknown }>;
metadata: Record<string, unknown>;
createdAt: string;
}
1.5 REST Endpoints
// ═══════════════════════════════════════
// PIPELINES
// ═══════════════════════════════════════
// GET /v1/pipelines
interface ListPipelinesParams extends PaginationParams {
status?: PipelineStatus;
stage?: PipelineStage;
priority?: Priority;
assigneeId?: string;
search?: string;
}
// Response: ApiResponse<Pipeline[]>
// POST /v1/pipelines
interface CreatePipelineBody {
name: string;
template?: PipelineTemplate; // default: "mcp-server-standard"
platform: string;
config?: Record<string, unknown>;
priority?: Priority; // default: "medium"
assigneeId?: string;
}
// Response: ApiResponse<Pipeline> (201)
// GET /v1/pipelines/:pipelineId
// Response: ApiResponse<Pipeline>
// PATCH /v1/pipelines/:pipelineId
interface UpdatePipelineBody {
priority?: Priority;
status?: PipelineStatus;
config?: Record<string, unknown>;
assigneeId?: string | null;
metadata?: Record<string, unknown>;
}
// Response: ApiResponse<Pipeline>
// DELETE /v1/pipelines/:pipelineId?archive=true
// Response: 204
// GET /v1/pipelines/:pipelineId/stages
// Response: ApiResponse<PipelineStageRecord[]>
// POST /v1/pipelines/:pipelineId/stages/advance
interface AdvanceStageBody {
targetStage?: PipelineStage; // default: next in sequence
skipValidation?: boolean; // default: false (admin only)
notes?: string;
}
// Response: ApiResponse<{ stage: PipelineStageRecord; tasksCreated: Task[] }>
// ═══════════════════════════════════════
// TASKS (Decision Queue)
// ═══════════════════════════════════════
// GET /v1/tasks
interface ListTasksParams extends PaginationParams {
status?: TaskStatus;
priority?: Priority;
type?: TaskType;
pipelineId?: string;
assigneeId?: string; // use "me" for current user
slaBreached?: boolean;
}
// Response: ApiResponse<Task[]>
// GET /v1/tasks/:taskId
// Response: ApiResponse<Task>
// POST /v1/tasks/:taskId/claim
// Response: ApiResponse<Task>
// POST /v1/tasks/:taskId/complete
interface CompleteTaskBody {
decision: TaskDecision;
notes?: string;
decisionData?: Record<string, unknown>;
}
// Response: ApiResponse<Task>
// POST /v1/tasks/:taskId/reassign
interface ReassignTaskBody {
assigneeId: string;
reason?: string;
}
// Response: ApiResponse<Task>
// POST /v1/tasks/bulk
interface BulkTaskBody {
action: "approve" | "reject" | "defer";
taskIds: string[];
notes?: string;
}
// Response: ApiResponse<{ succeeded: string[]; failed: Array<{ taskId: string; error: string }> }>
// GET /v1/tasks/stats
// Response: ApiResponse<TaskStats>
interface TaskStats {
pending: number;
inProgress: number;
blocked: number;
avgWaitTimeMinutes: number;
slaBreaches: number;
byPriority: Record<Priority, number>;
byType: Record<TaskType, number>;
}
// ═══════════════════════════════════════
// APPROVALS
// ═══════════════════════════════════════
// GET /v1/approvals
interface ListApprovalsParams extends PaginationParams {
status?: ApprovalStatus;
pipelineId?: string;
assignee?: string; // "me" for current user
}
// Response: ApiResponse<Approval[]>
// GET /v1/approvals/:approvalId
// Response: ApiResponse<Approval>
// POST /v1/approvals/:approvalId/approve
interface ApproveBody {
notes?: string;
conditions?: string[];
}
// Response: ApiResponse<Approval>
// POST /v1/approvals/:approvalId/reject
interface RejectBody {
reason: string; // required
requestedChanges?: string[];
}
// Response: ApiResponse<Approval>
// POST /v1/approvals/:approvalId/escalate
interface EscalateBody {
escalateTo?: string; // user ID
reason: string;
}
// Response: ApiResponse<Approval>
// ═══════════════════════════════════════
// AGENTS
// ═══════════════════════════════════════
// GET /v1/agents
// Response: ApiResponse<Agent[]>
// GET /v1/agents/:agentId
// Response: ApiResponse<Agent>
// POST /v1/agents/:agentId/heartbeat
interface HeartbeatBody {
status: AgentStatus;
metrics?: Record<string, number>;
currentTaskId?: string | null;
}
// Response: 204
// GET /v1/agents/:agentId/activity
interface AgentActivityParams {
since?: string; // ISO 8601
limit?: number; // default: 50
}
// Response: ApiResponse<AuditEntry[]>
// ═══════════════════════════════════════
// ASSETS
// ═══════════════════════════════════════
// GET /v1/pipelines/:pipelineId/assets
interface ListAssetsParams extends PaginationParams {
type?: AssetType;
stage?: PipelineStage;
}
// Response: ApiResponse<Asset[]>
// POST /v1/assets (multipart/form-data)
interface CreateAssetBody {
pipelineId: string;
stage?: PipelineStage;
type: AssetType;
file: File; // binary upload
metadata?: Record<string, unknown>;
}
// Response: ApiResponse<Asset> (201)
// GET /v1/assets/:assetId
// Response: ApiResponse<Asset>
// GET /v1/assets/:assetId/download
// Response: binary stream with Content-Type and Content-Disposition headers
// GET /v1/assets/:assetId/diff
interface DiffParams {
fromVersion: number;
toVersion?: number; // default: latest
}
// Response: ApiResponse<DiffSummary>
// ═══════════════════════════════════════
// NOTIFICATIONS
// ═══════════════════════════════════════
// GET /v1/notifications
interface ListNotificationsParams extends PaginationParams {
unread?: boolean;
type?: NotificationType;
}
// Response: ApiResponse<Notification[]>
// POST /v1/notifications/:id/read
// Response: 204
// POST /v1/notifications/read-all
// Response: 204
// ═══════════════════════════════════════
// WEBHOOKS
// ═══════════════════════════════════════
// POST /v1/webhooks
interface CreateWebhookBody {
url: string;
events: WebSocketEventType[];
secret: string;
}
// Response: ApiResponse<Webhook> (201)
// GET /v1/webhooks
// Response: ApiResponse<Webhook[]>
// DELETE /v1/webhooks/:id
// Response: 204
// GET /v1/webhooks/events
// Response: ApiResponse<WebSocketEventType[]>
// ═══════════════════════════════════════
// AUDIT LOG
// ═══════════════════════════════════════
// GET /v1/audit
interface AuditParams extends PaginationParams {
entityType?: string;
entityId?: string;
actorId?: string;
action?: string;
since?: string; // ISO 8601
until?: string; // ISO 8601
}
// Response: ApiResponse<AuditEntry[]>
// ═══════════════════════════════════════
// FEEDBACK (Learning System)
// ═══════════════════════════════════════
// POST /v1/feedback
// Body: FeedbackEvent (see §3.2)
// Response: ApiResponse<{ id: string }> (201)
// GET /v1/feedback
interface FeedbackParams extends PaginationParams {
pipelineId?: string;
modalType?: string;
since?: string;
decision?: string;
}
// Response: ApiResponse<FeedbackEvent[]>
// GET /v1/feedback/stats
// Response: ApiResponse<FeedbackStats> (see §6)
// ═══════════════════════════════════════
// DASHBOARD (aggregated data)
// ═══════════════════════════════════════
// GET /v1/dashboard/summary
// Response: ApiResponse<DashboardSummary>
interface DashboardSummary {
pipelines: {
active: number;
paused: number;
completed: number;
failed: number;
};
tasks: TaskStats;
agents: {
total: number;
active: number;
idle: number;
error: number;
offline: number;
};
recentActivity: AuditEntry[]; // last 20 events
slaStatus: {
onTrack: number;
warning: number;
breached: number;
};
}
1.6 WebSocket Contract
// ═══════════════════════════════════════
// CONNECTION
// ═══════════════════════════════════════
// Connect: wss://api.goosefactory.dev/v1/ws?token=<jwt>
// On connect error: { type: "error", error: { code: "UNAUTHORIZED", message: "..." } }
// ═══════════════════════════════════════
// CLIENT → SERVER MESSAGES
// ═══════════════════════════════════════
interface WsSubscribe {
type: "subscribe";
channels: WsChannel[];
}
interface WsUnsubscribe {
type: "unsubscribe";
channels: WsChannel[];
}
interface WsPing {
type: "ping";
}
type WsChannel =
| "pipeline:*" // All pipeline events
| `pipeline:${string}` // Specific pipeline by ID
| "tasks:*" // All task events
| "tasks:pending" // Only pending task events
| "agents:*" // All agent events
| "agents:health" // Only health/heartbeat events
| "deploys:*" // All deploy events
| "notifications:me" // Current user's notifications
;
// ═══════════════════════════════════════
// SERVER → CLIENT MESSAGES
// ═══════════════════════════════════════
interface WsEvent<T = unknown> {
type: WebSocketEventType;
timestamp: string; // ISO 8601
channel: WsChannel;
data: T;
}
interface WsSubscribed {
type: "subscribed";
channels: WsChannel[];
}
interface WsPong {
type: "pong";
}
// ═══════════════════════════════════════
// EVENT TYPES AND PAYLOADS
// ═══════════════════════════════════════
type WebSocketEventType =
// Pipeline lifecycle
| "pipeline.created"
| "pipeline.updated"
| "pipeline.stage_changed"
| "pipeline.completed"
| "pipeline.failed"
| "pipeline.blocked"
// Task lifecycle
| "task.created"
| "task.claimed"
| "task.completed"
| "task.sla_warning"
| "task.sla_breached"
| "task.escalated"
// Approval lifecycle
| "approval.pending"
| "approval.approved"
| "approval.rejected"
// Agent status
| "agent.status_changed"
| "agent.heartbeat"
// Asset events
| "asset.created"
| "asset.updated"
// Deploy events
| "deploy.started"
| "deploy.succeeded"
| "deploy.failed"
// Feedback events
| "feedback.received"
| "feedback.processed"
;
// Event payload types:
interface PipelineCreatedPayload {
pipeline: Pipeline;
}
interface PipelineStageChangedPayload {
pipelineId: string;
pipelineName: string;
fromStage: PipelineStage;
toStage: PipelineStage;
triggeredBy: string; // user or agent ID
}
interface PipelineBlockedPayload {
pipelineId: string;
pipelineName: string;
blockedAt: PipelineStage;
reason: string;
taskId: string; // The task that's blocking
}
interface TaskCreatedPayload {
task: Task;
}
interface TaskCompletedPayload {
task: Task;
decision: TaskDecision;
decidedBy: string;
}
interface TaskSlaPayload {
taskId: string;
taskTitle: string;
pipelineId: string | null;
slaDeadline: string;
minutesRemaining: number; // negative if breached
escalationLevel: number;
}
interface AgentStatusPayload {
agentId: string;
agentName: string;
fromStatus: AgentStatus;
toStatus: AgentStatus;
currentTaskId: string | null;
}
interface AgentHeartbeatPayload {
agentId: string;
agentName: string;
status: AgentStatus;
health: AgentHealth;
}
interface DeployPayload {
pipelineId: string;
pipelineName: string;
target: "staging" | "production";
version: string;
status: "started" | "succeeded" | "failed";
url?: string; // Deploy URL on success
error?: string; // Error message on failure
}
interface FeedbackReceivedPayload {
feedbackId: string;
pipelineId: string | null;
modalType: string;
decision: string | null;
}
2. MCP Tool Schemas
2.1 Tool Definitions
Every tool follows MCP SDK conventions: name, description, inputSchema (JSON Schema), and returns content blocks.
// ═══════════════════════════════════════
// TOOL: factory_get_pending_tasks
// ═══════════════════════════════════════
const factory_get_pending_tasks = {
name: "factory_get_pending_tasks",
description:
"Get all tasks requiring human attention, sorted by priority and SLA urgency. " +
"This is the operator's decision inbox — the most important view in the factory.",
inputSchema: {
type: "object" as const,
properties: {
pipeline_id: {
type: "string",
description: "Filter tasks to a specific pipeline (UUID)",
},
priority: {
type: "string",
enum: ["critical", "high", "medium", "low"],
description: "Filter by priority level",
},
assignee: {
type: "string",
description: "Filter by assignee. Use 'me' for current user.",
},
include_context: {
type: "boolean",
description: "Include full task context (diffs, test results). Default: true",
default: true,
},
limit: {
type: "number",
description: "Maximum tasks to return. Default: 20",
default: 20,
},
},
required: [],
},
};
// Return: text content with formatted task list + task count summary
// ═══════════════════════════════════════
// TOOL: factory_approve_task
// ═══════════════════════════════════════
const factory_approve_task = {
name: "factory_approve_task",
description:
"Approve a pending task or approval gate, allowing the pipeline to proceed. " +
"Use this when the operator says 'approve', 'LGTM', 'ship it', etc.",
inputSchema: {
type: "object" as const,
properties: {
task_id: {
type: "string",
description: "UUID of the task to approve",
},
notes: {
type: "string",
description: "Optional approval notes or conditions",
},
conditions: {
type: "array",
items: { type: "string" },
description: "Conditions that must be met post-approval",
},
},
required: ["task_id"],
},
};
// Return: text confirming approval + what pipeline action was triggered
// ═══════════════════════════════════════
// TOOL: factory_reject_task
// ═══════════════════════════════════════
const factory_reject_task = {
name: "factory_reject_task",
description:
"Reject a pending task with feedback. Sends pipeline back for rework. " +
"Use when the operator says 'reject', 'needs work', 'fix this', etc.",
inputSchema: {
type: "object" as const,
properties: {
task_id: {
type: "string",
description: "UUID of the task to reject",
},
reason: {
type: "string",
description: "Why this was rejected — required for feedback loop",
},
requested_changes: {
type: "array",
items: { type: "string" },
description: "Specific changes needed before re-submission",
},
severity: {
type: "string",
enum: ["minor", "major", "critical"],
description: "How serious the issues are. Default: major",
default: "major",
},
},
required: ["task_id", "reason"],
},
};
// Return: text confirming rejection + which stage the pipeline reverted to
// ═══════════════════════════════════════
// TOOL: factory_get_pipeline_status
// ═══════════════════════════════════════
const factory_get_pipeline_status = {
name: "factory_get_pipeline_status",
description:
"Get current state of all pipelines or a specific pipeline. " +
"Shows stage, progress, blockers, and timeline.",
inputSchema: {
type: "object" as const,
properties: {
pipeline_id: {
type: "string",
description: "Specific pipeline UUID. Omit for all active pipelines.",
},
status: {
type: "string",
enum: ["active", "paused", "completed", "failed", "all"],
description: "Filter by status. Default: active",
default: "active",
},
include_details: {
type: "boolean",
description: "Include tasks, assets, and stage history. Default: false",
default: false,
},
},
required: [],
},
};
// ═══════════════════════════════════════
// TOOL: factory_advance_stage
// ═══════════════════════════════════════
const factory_advance_stage = {
name: "factory_advance_stage",
description:
"Manually advance a pipeline to its next stage. Triggers validation checks. " +
"If validation fails, returns the failures instead of advancing.",
inputSchema: {
type: "object" as const,
properties: {
pipeline_id: {
type: "string",
description: "UUID of the pipeline to advance",
},
target_stage: {
type: "string",
enum: ["intake", "scaffolding", "building", "testing", "review", "staging", "production", "published"],
description: "Target stage. Default: next in sequence",
},
skip_validation: {
type: "boolean",
description: "Skip gate checks. Admin/owner only. Default: false",
default: false,
},
notes: {
type: "string",
description: "Notes for the stage transition",
},
},
required: ["pipeline_id"],
},
};
// ═══════════════════════════════════════
// TOOL: factory_assign_priority
// ═══════════════════════════════════════
const factory_assign_priority = {
name: "factory_assign_priority",
description: "Set or change priority on a pipeline or task. Recalculates SLA deadlines.",
inputSchema: {
type: "object" as const,
properties: {
entity_type: {
type: "string",
enum: ["pipeline", "task"],
},
entity_id: {
type: "string",
description: "UUID of the pipeline or task",
},
priority: {
type: "string",
enum: ["critical", "high", "medium", "low"],
},
reason: {
type: "string",
description: "Why the priority changed",
},
},
required: ["entity_type", "entity_id", "priority"],
},
};
// ═══════════════════════════════════════
// TOOL: factory_get_blockers
// ═══════════════════════════════════════
const factory_get_blockers = {
name: "factory_get_blockers",
description:
"Get all items that are blocked and why. The 'what's stuck' view. " +
"Includes suggested actions to unblock.",
inputSchema: {
type: "object" as const,
properties: {
pipeline_id: {
type: "string",
description: "Filter to a specific pipeline",
},
include_suggestions: {
type: "boolean",
description: "Include AI-generated suggestions for unblocking. Default: true",
default: true,
},
},
required: [],
},
};
// Return type:
interface BlockerInfo {
blockerType: "task" | "stage" | "agent" | "dependency";
entityId: string;
title: string;
pipelineId: string;
pipelineName: string;
priority: Priority;
hoursBlocked: number;
blockReason: string;
suggestedAction?: string;
}
// ═══════════════════════════════════════
// TOOL: factory_run_tests
// ═══════════════════════════════════════
const factory_run_tests = {
name: "factory_run_tests",
description: "Trigger test suite for a pipeline's current build.",
inputSchema: {
type: "object" as const,
properties: {
pipeline_id: {
type: "string",
description: "UUID of the pipeline",
},
test_type: {
type: "string",
enum: ["unit", "integration", "e2e", "all"],
description: "Which tests to run. Default: all",
default: "all",
},
environment: {
type: "string",
enum: ["local", "ci"],
description: "Where to run. Default: ci",
default: "ci",
},
},
required: ["pipeline_id"],
},
};
// ═══════════════════════════════════════
// TOOL: factory_deploy
// ═══════════════════════════════════════
const factory_deploy = {
name: "factory_deploy",
description:
"Deploy a pipeline's build to staging or production. " +
"Production requires prior staging deploy + approval.",
inputSchema: {
type: "object" as const,
properties: {
pipeline_id: {
type: "string",
description: "UUID of the pipeline to deploy",
},
target: {
type: "string",
enum: ["staging", "production"],
},
version: {
type: "string",
description: "Specific version. Default: latest build",
},
dry_run: {
type: "boolean",
description: "Preview what would happen without deploying. Default: false",
default: false,
},
},
required: ["pipeline_id", "target"],
},
};
// ═══════════════════════════════════════
// TOOL: factory_search
// ═══════════════════════════════════════
const factory_search = {
name: "factory_search",
description: "Search across pipelines, tasks, assets, and audit logs.",
inputSchema: {
type: "object" as const,
properties: {
query: {
type: "string",
description: "Search query text",
},
entity_types: {
type: "array",
items: { type: "string", enum: ["pipeline", "task", "asset", "audit"] },
description: "Which entity types to search. Default: all",
},
limit: {
type: "number",
description: "Max results. Default: 10",
default: 10,
},
},
required: ["query"],
},
};
// ═══════════════════════════════════════
// TOOL: factory_create_pipeline
// ═══════════════════════════════════════
const factory_create_pipeline = {
name: "factory_create_pipeline",
description: "Initialize a new MCP server pipeline from a template.",
inputSchema: {
type: "object" as const,
properties: {
name: {
type: "string",
description: "Pipeline name (e.g., 'ghl-mcp-server')",
},
platform: {
type: "string",
description: "Target platform (e.g., 'go-high-level', 'shopify')",
},
template: {
type: "string",
enum: ["mcp-server-standard", "mcp-server-minimal", "mcp-server-enterprise"],
description: "Pipeline template. Default: mcp-server-standard",
default: "mcp-server-standard",
},
priority: {
type: "string",
enum: ["critical", "high", "medium", "low"],
description: "Initial priority. Default: medium",
default: "medium",
},
},
required: ["name", "platform"],
},
};
2.2 MCP Resource URIs
// ═══════════════════════════════════════
// RESOURCES (read-only, subscribable)
// ═══════════════════════════════════════
// Static resources (fixed URI):
// factory://dashboard/summary
// Returns: DashboardSummary (see §1.5)
// MIME: application/json
// Updates: on any pipeline/task/agent event
// factory://config/templates
// Returns: PipelineTemplateConfig[]
// MIME: application/json
// Updates: rarely (config changes)
interface PipelineTemplateConfig {
id: PipelineTemplate;
name: string;
description: string;
stages: PipelineStage[];
defaultConfig: Record<string, unknown>;
requiredAssets: AssetType[];
validationRules: ValidationRule[];
}
// Dynamic resources (parameterized URI):
// factory://pipelines/{pipelineId}/state
// Returns: PipelineDetailState
// MIME: application/json
// Updates: on pipeline.* events for this pipeline
interface PipelineDetailState {
pipeline: Pipeline;
stages: PipelineStageRecord[];
currentTasks: Task[];
recentAssets: Asset[];
blockers: BlockerInfo[];
timeline: AuditEntry[]; // last 50 events for this pipeline
}
// factory://servers/{serverName}/status
// Returns: ServerStatus
// MIME: application/json
interface ServerStatus {
name: string;
pipelineId: string;
stage: PipelineStage;
health: "healthy" | "degraded" | "failing" | "unknown";
deployments: {
staging: DeploymentInfo | null;
production: DeploymentInfo | null;
};
lastTestRun: TestResults | null;
metrics: Record<string, number>;
}
interface DeploymentInfo {
version: string;
deployedAt: string;
url: string;
status: "running" | "stopped" | "failed";
}
// factory://pipelines/{pipelineId}/test-results
// Returns: TestResults (see §1.4)
// MIME: application/json
// factory://pipelines/{pipelineId}/build-logs
// Returns: string (raw build output)
// MIME: text/plain
2.3 MCP Prompts
// ═══════════════════════════════════════
// PROMPT: review_server
// ═══════════════════════════════════════
const review_server = {
name: "review_server",
description:
"Pull all context needed to review an MCP server: code quality, " +
"test results, docs, deployment readiness. Returns a structured review prompt.",
arguments: [
{
name: "server_name",
description: "Name of the MCP server to review",
required: true,
},
],
};
// Returns: messages[] containing pipeline status, test results, asset inventory,
// open tasks, quality standards comparison, review checklist
// ═══════════════════════════════════════
// PROMPT: whats_needs_attention
// ═══════════════════════════════════════
const whats_needs_attention = {
name: "whats_needs_attention",
description:
"Summary of everything needing human attention right now, prioritized by urgency.",
arguments: [
{
name: "scope",
description: "Filter scope: 'all' | 'my-tasks' | 'critical-only'. Default: all",
required: false,
},
],
};
// Returns: messages[] with SLA breaches, pending approvals, blocked pipelines,
// failed tests/deploys, agent health issues, suggested next actions
// ═══════════════════════════════════════
// PROMPT: deploy_checklist
// ═══════════════════════════════════════
const deploy_checklist = {
name: "deploy_checklist",
description: "Pre-deployment review checklist for an MCP server.",
arguments: [
{
name: "pipeline_id",
description: "UUID of the pipeline being deployed",
required: true,
},
{
name: "target",
description: "'staging' or 'production'",
required: true,
},
],
};
// Returns: messages[] with structured checklist:
// tests, code review, README, TOOLS.md, env vars, secrets, perf, rollback plan
// ═══════════════════════════════════════
// PROMPT: pipeline_retrospective
// ═══════════════════════════════════════
const pipeline_retrospective = {
name: "pipeline_retrospective",
description: "Generate a retrospective analysis of a completed pipeline.",
arguments: [
{
name: "pipeline_id",
description: "UUID of the completed pipeline",
required: true,
},
],
};
// Returns: messages[] with timeline, bottlenecks, quality metrics, lessons learned
2.4 MCP App Hosting Contract
// ═══════════════════════════════════════
// MCP APP REGISTRATION
// ═══════════════════════════════════════
// MCP tools can return UI content using Goose's MCP UI rendering system.
// The Factory MCP server uses this for rich HITL modals.
// Tool response format for rendering a modal:
interface McpToolResponseWithUI {
_meta: {
goose: {
toolUI: {
displayType: "inline" | "sidecar"; // inline = in chat, sidecar = side panel
name: string; // Display name for the UI
renderer: "mcp-ui"; // Must be "mcp-ui"
};
};
};
content: McpUIResource[];
}
// UI resource format (one per content block):
interface McpUIResource {
uri: string; // e.g., "ui://hitl-modal"
mimeType: "text/html"; // Always HTML for our modals
text: string; // The HTML content (self-contained)
}
// The tool creates the resource using @mcp-ui/client:
// createUIResource({
// uri: "ui://factory-modal",
// content: { type: "rawHtml", htmlString: modalHtml },
// encoding: "text",
// })
// ═══════════════════════════════════════
// MODAL REGISTRATION (internal to MCP server)
// ═══════════════════════════════════════
// Each modal type is registered with its capabilities:
interface ModalRegistration {
id: string; // e.g., "traffic-light"
name: string; // e.g., "Traffic Light"
description: string;
version: string; // semver
// When to use this modal
suitableFor: ModalUseCase[];
// What data it needs
requiredContext: ModalContextField[];
optionalContext: ModalContextField[];
// What data it produces
outputSchema: {
feedbackTypes: FeedbackType[];
capturesMetrics: string[]; // e.g., ["response_time", "hover_journey"]
};
// Display preferences
display: {
preferredType: "inline" | "sidecar";
minWidth: number; // pixels
minHeight: number;
supportsDarkMode: boolean;
supportsMobile: boolean;
};
}
type ModalUseCase =
| "binary_decision" // pass/fail, approve/reject
| "batch_review" // multiple items at once
| "multi_dimensional" // rate across multiple axes
| "comparison" // A vs B
| "subjective" // emotional/gut reaction
| "high_stakes" // critical decisions
| "annotation" // mark specific regions
| "estimation" // effort/risk estimation
| "triage" // multi-pipeline overview
| "retrospective" // reflection/improvement
| "verification"; // checklist confirmation
type ModalContextField =
| "pipeline_id"
| "pipeline_name"
| "item_id"
| "item_name"
| "deliverable_preview"
| "deliverable_content"
| "test_results"
| "diff_summary"
| "metrics"
| "items_batch" // for batch modals: array of items
| "content_before" // for comparison modals
| "content_after"
| "checklist_items" // for verification modals
| "estimation_question"
| "persona" // for voice-of-customer
| "dimensions" // for multi-dimensional modals
;
3. Modal Data Contract
3.1 Host → Modal Communication (Context Injection)
// The MCP App iframe host passes context data INTO the modal via URL parameters
// and a global JavaScript object injected before the modal HTML loads.
// ═══════════════════════════════════════
// URL PARAMETERS (simple values)
// ═══════════════════════════════════════
// The modal's src URL includes query params:
// ui://factory-modal?pipeline_id=xxx&item_id=yyy&modal_type=traffic-light
// ═══════════════════════════════════════
// INJECTED CONTEXT OBJECT (complex data)
// ═══════════════════════════════════════
// The host injects a global object BEFORE the modal renders.
// The modal accesses it via window.__FACTORY_CONTEXT__
interface FactoryModalContext {
// Identity
modalType: string; // e.g., "traffic-light"
modalVersion: string; // semver
sessionId: string; // Factory session ID
// What's being reviewed
pipelineId: string;
pipelineName: string;
itemId: string;
itemName: string;
// Content (varies by modal type)
deliverablePreview?: string; // Short text preview
deliverableContent?: string; // Full content (code, text, etc.)
contentBefore?: string; // For comparison modals
contentAfter?: string;
// Supporting data
testResults?: TestResults;
diffSummary?: DiffSummary;
metrics?: Record<string, number>;
// Batch data (for multi-item modals)
items?: ModalItem[];
// Configuration
dimensions?: ModalDimension[]; // For multi-dimensional modals
checklistItems?: ChecklistItem[]; // For verification modals
estimationQuestion?: string;
persona?: ModalPersona;
timerSeconds?: number; // For timed modals
// Theme
theme: "dark" | "light";
accentColor: string; // hex color
}
interface ModalItem {
id: string;
name: string;
preview: string;
content?: string;
metadata?: Record<string, unknown>;
}
interface ModalDimension {
id: string; // e.g., "code_quality"
name: string; // e.g., "Code Quality"
description?: string;
weight: number; // Importance multiplier (1-3)
}
interface ChecklistItem {
id: string;
label: string;
description?: string;
aiAssessment?: string; // What the AI thinks about this item
aiPasses?: boolean; // AI's pre-assessment
}
interface ModalPersona {
name: string;
role: string;
context: string;
avatarEmoji: string;
}
3.2 Modal → Host Communication (postMessage API)
// ═══════════════════════════════════════
// EVERY modal posts decisions back via:
// window.parent.postMessage(message, "*");
// ═══════════════════════════════════════
// === Discriminated Union of all message types ===
type ModalMessage =
| ModalResponse
| ModalReady
| ModalResize
| ModalClose
| ModalError;
// ─── READY (modal loaded and interactive) ───
interface ModalReady {
type: "factory_modal_ready";
modalType: string;
version: string;
}
// ─── RESIZE (modal wants to change size) ───
interface ModalResize {
type: "factory_modal_resize";
width?: number;
height: number;
}
// ─── CLOSE (modal wants to close without submitting) ───
interface ModalClose {
type: "factory_modal_close";
reason: "cancelled" | "timeout" | "error";
}
// ─── ERROR (modal hit an error) ───
interface ModalError {
type: "factory_modal_error";
error: string;
details?: unknown;
}
// ─── RESPONSE (the main data submission) ───
interface ModalResponse {
type: "factory_modal_response";
// Identity
modalType: string; // Which modal was used
modalVersion: string;
pipelineId: string;
itemId: string;
sessionId: string;
// Timing
timestamp: string; // ISO 8601
responseTimeMs: number; // Time from modal open to submission
// The feedback data (see FeedbackEvent below)
feedback: FeedbackPayload;
// Hidden behavioral metrics (ALWAYS collected)
meta: ModalMetrics;
}
// ═══════════════════════════════════════
// FEEDBACK PAYLOAD
// (the meaningful data from the modal)
// ═══════════════════════════════════════
interface FeedbackPayload {
// Decision (present in most modals)
decision?: DecisionFeedback;
// Scores (multi-dimensional modals)
scores?: DimensionScore[];
// Free text
freeText?: FreeTextFeedback;
// Comparison (A/B modals)
comparison?: ComparisonFeedback;
// Annotations (spotlight/code review)
annotations?: Annotation[];
// Confidence (confidence meter or paired)
confidence?: ConfidenceFeedback;
// Estimation (priority poker)
estimation?: EstimationFeedback;
// Batch decisions (swipe/speed round)
batchDecisions?: BatchDecision[];
// Retrospective (start/stop/continue)
retrospective?: RetrospectiveFeedback;
// Ranking (ranking arena)
ranking?: RankingFeedback;
// Checklist (checklist ceremony)
checklist?: ChecklistFeedback;
// Temperature (thermometer)
temperature?: TemperatureFeedback;
// Custom data (any modal-specific data)
custom?: Record<string, unknown>;
}
// ═══════════════════════════════════════
// THE 8 FEEDBACK TYPES (atomic units)
// ═══════════════════════════════════════
// 1. DECISION
interface DecisionFeedback {
decision: "approved" | "rejected" | "needs_work" | "deferred" | "skipped";
reason?: string;
severity?: "minor" | "moderate" | "major" | "critical";
blockers?: string[];
suggestions?: string[];
tags?: string[]; // e.g., ["naming", "logic", "style"]
}
// 2. DIMENSION SCORE
interface DimensionScore {
dimension: QualityDimension;
score: number; // 1-10 (integer or float)
weight?: number; // How much the operator cares (learned)
comment?: string;
}
type QualityDimension =
| "code_quality"
| "design_aesthetics"
| "design_ux"
| "test_coverage"
| "documentation"
| "architecture"
| "error_handling"
| "performance"
| "security"
| "creativity"
| "completeness"
| "naming"
| "dx";
// 3. FREE TEXT
interface FreeTextFeedback {
liked?: string;
disliked?: string;
generalNotes?: string;
}
// 4. COMPARISON
interface ComparisonFeedback {
preferred: "A" | "B" | "neither" | "both_good";
reason?: string;
preferenceStrength: "slight" | "moderate" | "strong" | "overwhelming";
winningFactors?: string[];
perDimension?: Array<{
dimension: QualityDimension;
winner: "A" | "B" | "tie";
}>;
timeOnA_ms?: number;
timeOnB_ms?: number;
}
// 5. ANNOTATION
interface Annotation {
type: "praise" | "criticism" | "question" | "suggestion";
target: {
file?: string;
lineStart?: number;
lineEnd?: number;
charStart?: number;
charEnd?: number;
selector?: string; // CSS selector for design
region?: string; // Named region
};
text: string;
severity?: "nit" | "minor" | "major" | "critical";
category?: string;
}
// 6. CONFIDENCE
interface ConfidenceFeedback {
confidencePercent: number; // 0-100
zone: "guess" | "educated_guess" | "confident" | "certain";
wouldDelegateToAI: boolean;
needsExpertReview: boolean;
lowConfidenceReason?: string;
isTrainingExample?: boolean; // High-confidence decisions can train
}
// 7. ESTIMATION
interface EstimationFeedback {
estimate: number | "unknown" | "break"; // Fibonacci: 1,2,3,5,8,13,21
context?: string;
hoveredValues?: number[]; // Which cards were considered
}
// 8. BATCH DECISION
interface BatchDecision {
itemId: string;
decision: "approve" | "reject" | "love" | "skip";
timeMs: number; // Time spent on this item
flippedForDetails: boolean; // Did they look at full details?
}
// ─── COMPOSITE TYPES ───
interface RankingFeedback {
finalRanking: string[]; // Ordered array of item IDs (best first)
rankingChanges: number; // Number of reorder operations
lockedItems: string[]; // Items operator was most confident about
expandedItems: string[]; // Items that needed closer inspection
perItemNotes?: Record<string, string>;
}
interface RetrospectiveFeedback {
start: RetrospectiveNote[];
stop: RetrospectiveNote[];
continue_: RetrospectiveNote[]; // "continue" is reserved word, use continue_
}
interface RetrospectiveNote {
text: string;
priority?: "urgent" | "important" | "idea";
}
interface ChecklistFeedback {
checks: Record<string, boolean>; // { criterionId: checked }
notes: Record<string, string>; // { criterionId: note } for unchecked items
checkOrder: string[]; // Order items were checked
allClear: boolean;
completionRate: number; // 0-1
}
interface TemperatureFeedback {
temperature: number; // 0-100
zone: "freezing" | "cold" | "lukewarm" | "warm" | "hot" | "blazing";
drivers: string[]; // What drove the rating
dragJourney: number[]; // Temperature values during drag
}
// ═══════════════════════════════════════
// HIDDEN BEHAVIORAL METRICS
// (collected by every modal automatically)
// ═══════════════════════════════════════
interface ModalMetrics {
// Timing
timeToFirstInteractionMs: number;
timeToDecisionMs: number;
// Engagement
fieldsModified: string[];
scrollDepth?: number; // 0-1 (how much content was viewed)
revisits: number; // Times the modal was re-focused
totalInteractions: number; // Click/tap count
// Context
viewportSize: { width: number; height: number };
deviceType: "desktop" | "mobile";
// Hover/attention data (modal-specific)
hoverJourney?: Array<{ target: string; durationMs: number }>;
hesitationPoints?: Array<{ target: string; durationMs: number }>;
}
3.3 FeedbackEvent (Full Storage Schema)
// The complete event stored by the learning system.
// Combines ModalResponse with enriched metadata.
interface FeedbackEvent {
// Identity
id: string; // UUID v7 (time-sortable)
timestamp: string; // ISO 8601
sessionId: string;
modalType: string;
modalVersion: string;
// What was reviewed
workProduct: WorkProductRef;
pipelineId: string | null;
pipelineStage: PipelineStage | null;
mcpServerType?: string;
// The feedback (from ModalResponse.feedback)
feedback: FeedbackPayload;
// Behavioral metrics (from ModalResponse.meta)
meta: ModalMetrics & EnrichedMeta;
}
interface WorkProductRef {
type: "mcp_server" | "code_module" | "design" | "test_suite" | "documentation" | "pipeline_config";
id: string;
version: string; // Git SHA or version tag
path?: string;
summary: string;
bubaConfidence: number; // 0-1: AI confidence before review
bubaScorePrediction?: number; // 1-10: predicted human score
generationContext: {
promptHash: string;
memorySnapshot: string[];
modelUsed: string;
generationTimeMs: number;
iterationCount: number;
};
}
interface EnrichedMeta {
timeOfDay: "morning" | "afternoon" | "evening" | "night";
dayOfWeek: string;
sessionFatigue: number; // Nth modal in this session
concurrentModals: number;
// NLP enrichment (filled by processing pipeline)
extractedThemes?: string[];
sentiment?: number; // -1 to 1
actionableItems?: string[];
predictionDelta?: number; // actual - predicted score
}
4. UI Component Contracts
4.1 Decision Queue Sidebar
// ═══════════════════════════════════════
// DecisionQueue Component
// Location: packages/desktop/ui/desktop/src/components/factory/DecisionQueue.tsx
// ═══════════════════════════════════════
interface DecisionQueueProps {
// Data
tasks: Task[];
selectedTaskId: string | null;
// Loading/error state
isLoading: boolean;
error: string | null;
// Callbacks
onSelectTask: (taskId: string) => void;
onApproveTask: (taskId: string) => void;
onRejectTask: (taskId: string, reason: string) => void;
onDeferTask: (taskId: string) => void;
onBatchApprove: (taskIds: string[]) => void;
onRefresh: () => void;
// Filters (controlled)
filters: DecisionQueueFilters;
onFiltersChange: (filters: DecisionQueueFilters) => void;
}
interface DecisionQueueFilters {
priority?: Priority;
type?: TaskType;
slaBreached?: boolean;
search?: string;
}
// ─── Data shape for queue items ───
interface DecisionQueueItem {
id: string;
title: string;
type: TaskType;
priority: Priority;
pipelineName: string;
stageName: PipelineStage;
// SLA
slaDeadline: string | null;
slaBreached: boolean;
minutesWaiting: number;
// Quick context
previewText: string;
testSummary?: string; // e.g., "47/47 ✅"
// Status indicators
isSelected: boolean;
isBatchSelected: boolean;
createdAt: string;
}
// ─── Keyboard shortcuts ───
// j / ArrowDown → select next task
// k / ArrowUp → select previous task
// a → approve selected task
// r → open reject dialog for selected
// d → defer selected task
// Enter → open context panel for selected
// Shift+a → toggle batch select
// Ctrl+Shift+a → batch approve all selected
4.2 Context Panel
// ═══════════════════════════════════════
// ContextPanel Component
// Location: packages/desktop/ui/desktop/src/components/factory/ContextPanel.tsx
// ═══════════════════════════════════════
interface ContextPanelProps {
// Data
task: Task | null;
pipeline: Pipeline | null;
stages: PipelineStageRecord[];
assets: Asset[];
auditTrail: AuditEntry[];
// Loading
isLoading: boolean;
// Actions
onApprove: (notes?: string, conditions?: string[]) => void;
onReject: (reason: string, requestedChanges?: string[]) => void;
onDefer: () => void;
onEscalate: (reason: string) => void;
onRunTests: () => void;
onViewDiff: (assetId: string) => void;
onOpenModal: (modalType: string) => void;
// Tab navigation
activeTab: ContextTab;
onTabChange: (tab: ContextTab) => void;
}
type ContextTab = "overview" | "diff" | "tests" | "assets" | "timeline" | "modal";
// The context panel renders different content based on activeTab:
// - "overview" → Task summary, pipeline state, key metrics
// - "diff" → Code diff viewer (DiffSummary rendered)
// - "tests" → Test results detail (TestResults rendered)
// - "assets" → Asset list with download links
// - "timeline" → Audit trail for this pipeline
// - "modal" → Embedded HITL modal (iframe)
4.3 Pipeline Kanban
// ═══════════════════════════════════════
// PipelineKanban Component
// Location: packages/desktop/ui/desktop/src/components/factory/PipelineKanban.tsx
// ═══════════════════════════════════════
interface PipelineKanbanProps {
// Data
pipelines: Pipeline[];
// Callbacks
onSelectPipeline: (pipelineId: string) => void;
onAdvanceStage: (pipelineId: string, targetStage: PipelineStage) => void;
onChangePriority: (pipelineId: string, priority: Priority) => void;
// View options
groupBy: "stage" | "priority" | "assignee";
onGroupByChange: (groupBy: string) => void;
}
// ─── Kanban stages (columns) ───
const KANBAN_STAGES: KanbanStageConfig[] = [
{ stage: "intake", label: "📥 Intake", color: "#666" },
{ stage: "scaffolding", label: "🏗️ Scaffolding", color: "#888" },
{ stage: "building", label: "🔨 Building", color: "#4488FF" },
{ stage: "testing", label: "🧪 Testing", color: "#FF8844" },
{ stage: "review", label: "👁️ Review ★", color: "#FFD700", isGate: true },
{ stage: "staging", label: "🚀 Staging ★", color: "#00AAFF", isGate: true },
{ stage: "production", label: "🌍 Production ★", color: "#FF4444", isGate: true },
{ stage: "published", label: "✅ Published", color: "#00CC55" },
];
interface KanbanStageConfig {
stage: PipelineStage;
label: string;
color: string;
isGate?: boolean; // Requires human approval
}
// ─── Kanban card data shape ───
interface KanbanCard {
id: string; // pipeline ID
name: string;
platform: string;
priority: Priority;
status: PipelineStatus;
// Progress
currentStage: PipelineStage;
stageIndex: number; // 0-7
progressPercent: number; // 0-100
// Indicators
hasBlockers: boolean;
hasSlaWarning: boolean;
hasSlaBreached: boolean;
pendingTaskCount: number;
// Quick stats
testsPassing?: string; // "47/47" or "38/47"
coveragePercent?: number;
lastActivity: string; // ISO 8601
assigneeName?: string;
}
4.4 MCP App Iframe Host
// ═══════════════════════════════════════
// McpAppHost Component
// Location: packages/desktop/ui/desktop/src/components/factory/McpAppHost.tsx
// ═══════════════════════════════════════
interface McpAppHostProps {
// What to render
htmlContent: string; // Self-contained HTML from MCP tool response
context: FactoryModalContext; // Injected into iframe as window.__FACTORY_CONTEXT__
// Display
displayType: "inline" | "sidecar";
maxWidth?: number;
maxHeight?: number;
// Lifecycle callbacks
onReady: () => void; // Modal sent factory_modal_ready
onResponse: (response: ModalResponse) => void; // Modal submitted feedback
onResize: (height: number) => void; // Modal requested resize
onClose: (reason: string) => void; // Modal requested close
onError: (error: string) => void; // Modal reported error
}
// ─── Host implementation requirements ───
// 1. Create a sandboxed iframe:
// <iframe sandbox="allow-scripts allow-forms" srcdoc={processedHtml} />
//
// 2. Inject context BEFORE modal HTML:
// Prepend: <script>window.__FACTORY_CONTEXT__ = ${JSON.stringify(context)};</script>
//
// 3. Listen for postMessage events:
// window.addEventListener("message", (event) => {
// if (event.data.type === "factory_modal_response") { onResponse(event.data); }
// if (event.data.type === "factory_modal_ready") { onReady(); }
// if (event.data.type === "factory_modal_resize") { onResize(event.data.height); }
// if (event.data.type === "factory_modal_close") { onClose(event.data.reason); }
// if (event.data.type === "factory_modal_error") { onError(event.data.error); }
// });
//
// 4. On factory_modal_response:
// a. Validate response against FeedbackPayload schema
// b. Forward to API: POST /v1/feedback
// c. Close the modal iframe
// d. Advance to next task in queue
//
// 5. On timeout (no response within configurable limit):
// a. Post "factory_modal_timeout" into the iframe
// b. Wait 5s for graceful close
// c. Force close and mark task as "deferred"
4.5 WebSocket Client
// ═══════════════════════════════════════
// useFactoryWebSocket Hook
// Location: packages/desktop/ui/desktop/src/hooks/useWebSocket.ts
// ═══════════════════════════════════════
interface UseFactoryWebSocketOptions {
token: string;
channels: WsChannel[];
onEvent: (event: WsEvent) => void;
// Reconnection
maxRetries?: number; // default: 10
retryDelayMs?: number; // default: 1000, doubles each retry
maxRetryDelayMs?: number; // default: 30000
}
interface UseFactoryWebSocketReturn {
isConnected: boolean;
subscribe: (channels: WsChannel[]) => void;
unsubscribe: (channels: WsChannel[]) => void;
disconnect: () => void;
}
// ─── How the WebSocket client updates state ───
// The hook is used in a FactoryContext provider that maintains all factory state.
// On each WsEvent, the provider updates its state:
type FactoryAction =
| { type: "PIPELINE_UPDATED"; pipeline: Pipeline }
| { type: "TASK_CREATED"; task: Task }
| { type: "TASK_COMPLETED"; task: Task }
| { type: "TASK_SLA_WARNING"; payload: TaskSlaPayload }
| { type: "AGENT_STATUS"; payload: AgentStatusPayload }
| { type: "DEPLOY_UPDATE"; payload: DeployPayload }
| { type: "NOTIFICATION"; notification: Notification }
| { type: "SET_TASKS"; tasks: Task[] }
| { type: "SET_PIPELINES"; pipelines: Pipeline[] }
| { type: "SET_AGENTS"; agents: Agent[] }
;
interface FactoryState {
// Core data
pipelines: Pipeline[];
tasks: Task[];
agents: Agent[];
notifications: Notification[];
// Derived
pendingTaskCount: number;
slaBreachCount: number;
// Connection
wsConnected: boolean;
// Loading
isInitialLoading: boolean;
lastUpdated: string;
}
// Event → State mapping:
//
// "pipeline.created" → add to pipelines[]
// "pipeline.stage_changed"→ update pipeline in pipelines[]
// "pipeline.completed" → update pipeline status
// "task.created" → add to tasks[], increment pendingTaskCount
// "task.completed" → update task in tasks[], decrement pendingTaskCount
// "task.sla_warning" → update task slaWarning flag
// "task.sla_breached" → update task, increment slaBreachCount
// "agent.status_changed" → update agent in agents[]
// "deploy.succeeded" → update pipeline deployments
// "feedback.received" → (optional) show toast notification
5. Rust Backend Contracts
5.1 Factory Extension Registration
// ═══════════════════════════════════════
// How the Factory extension registers with goosed
// Location: crates/goose-mcp/src/factory/mod.rs
// ═══════════════════════════════════════
// The Factory MCP Server is registered as a built-in extension
// in the goosed binary, similar to how "developer" and "memory"
// extensions are registered.
// In crates/goose-mcp/src/lib.rs, add:
// pub mod factory;
// In ui/desktop/src/built-in-extensions.json, add:
{
"id": "factory",
"name": "Factory Command Center",
"description": "AI Factory pipeline management — decision queue, approvals, and deployments",
"enabled": true,
"type": "builtin"
}
// The Factory extension provides:
// - 11 MCP tools (§2.1)
// - 6 MCP resources (§2.2)
// - 4 MCP prompts (§2.3)
// - MCP App UI rendering for HITL modals (§2.4)
// Extension config in ~/.config/goose/config.yaml:
# Factory extension configuration
extensions:
factory:
name: Factory Command Center
type: builtin
enabled: true
config:
api_base_url: "https://api.goosefactory.dev/v1"
ws_url: "wss://api.goosefactory.dev/v1/ws"
# Auth: uses the user's JWT from the desktop app login
default_priority: "medium"
auto_advance_low_risk: true
sla_defaults:
critical: 3600 # 1 hour
high: 14400 # 4 hours
medium: 86400 # 24 hours
low: 259200 # 72 hours
5.2 Permission/Approval Flow Modifications
// ═══════════════════════════════════════
// GooseFactory modifies Goose's built-in permission system
// to route factory operations through the approval queue.
// ═══════════════════════════════════════
// Goose permission modes (existing):
// - auto: Full autonomy
// - approve: Confirm before any tool use
// - smart_approve: Risk-based (auto-approve low-risk, flag high-risk)
// - chat: No tool use
// GooseFactory additions:
// - factory_smart: Like smart_approve but routes high-risk decisions
// through the Factory decision queue instead of inline prompts.
// Risk classification for factory tools:
interface FactoryToolRiskClassification {
// LOW RISK — auto-execute in factory_smart mode
lowRisk: [
"factory_get_pending_tasks",
"factory_get_pipeline_status",
"factory_get_blockers",
"factory_search",
];
// MEDIUM RISK — queue for review but don't block
mediumRisk: [
"factory_approve_task", // Approving items
"factory_reject_task", // Rejecting items
"factory_advance_stage", // Stage advancement
"factory_assign_priority", // Priority changes
"factory_run_tests", // Triggering tests
"factory_create_pipeline", // Creating new pipelines
];
// HIGH RISK — always require explicit human approval
highRisk: [
"factory_deploy", // Deployment (especially production)
];
}
// The permission check flow:
//
// 1. Tool call received by goosed
// 2. goosed checks permission mode (factory_smart)
// 3. If lowRisk → execute immediately
// 4. If mediumRisk → execute, log to audit, notify operator
// 5. If highRisk → create Task in decision queue, block until approved
// 6. For "factory_deploy" with target="production":
// → ALWAYS creates a Task with type="approval"
// → ALWAYS requires explicit human approval
// → Approval task includes deploy checklist (prompt: deploy_checklist)
5.3 Config File Format Changes
# ═══════════════════════════════════════
# ~/.config/goose/config.yaml — Factory additions
# ═══════════════════════════════════════
# Existing Goose config fields are preserved.
# Factory-specific fields are namespaced under `factory:`.
factory:
# API connection
api_url: "https://api.goosefactory.dev/v1"
ws_url: "wss://api.goosefactory.dev/v1/ws"
# Auth (managed by desktop app login flow)
# JWT stored in system keychain, not in config file
# Operator preferences
operator:
name: "Jake"
default_view: "decision-queue" # "decision-queue" | "kanban" | "chat"
keyboard_shortcuts: true
sound_notifications: true
desktop_notifications: true
# SLA configuration
sla:
critical_seconds: 3600 # 1 hour
high_seconds: 14400 # 4 hours
medium_seconds: 86400 # 24 hours
low_seconds: 259200 # 72 hours
warning_threshold_percent: 25 # Warn at 25% time remaining
# Escalation
escalation:
enabled: true
channels: ["dashboard", "discord"]
sms_on_breach: false
auto_escalate_after_breach_minutes: 120
# Learning system
learning:
enabled: true
feedback_storage_path: "~/.config/goose/factory/feedback"
memory_files_path: "~/.config/goose/factory/memory"
auto_update_goosehints: true
min_data_points_for_rules: 5
confidence_calibration: true
# Notification batching
notifications:
batch_interval_seconds: 300 # Batch similar notifications over 5min
quiet_hours_start: "22:00" # No non-critical notifications
quiet_hours_end: "08:00"
peak_hours: ["09:00-11:00", "14:00-16:00"] # Prefer sending during these
# Auto-approval (learned thresholds)
auto_approval:
enabled: false # Disabled until Level 2+ reached
min_approval_rate: 0.90 # 90% historical approval for this task type
min_data_points: 25
excluded_stages: ["production"] # Never auto-approve production
5.4 Protocol Handler Registration
// ═══════════════════════════════════════
// factory:// Protocol Handler
// Location: ui/desktop/src/main.ts
// ═══════════════════════════════════════
// Replace goose:// with factory:// protocol handler.
// Supports deep links for quick actions.
// Protocol URI patterns:
type FactoryProtocolURI =
| `factory://approve?task_id=${string}`
| `factory://reject?task_id=${string}`
| `factory://pipeline?id=${string}`
| `factory://task?id=${string}`
| `factory://dashboard`
| `factory://review?server_name=${string}`
| `factory://deploy?pipeline_id=${string}&target=${"staging" | "production"}`
;
// Electron main process handler:
//
// app.setAsDefaultProtocolClient("factory");
//
// app.on("open-url", (event, url) => {
// event.preventDefault();
// const parsed = new URL(url);
// switch (parsed.hostname) {
// case "approve":
// handleApproveDeepLink(parsed.searchParams.get("task_id"));
// break;
// case "reject":
// handleRejectDeepLink(parsed.searchParams.get("task_id"));
// break;
// case "pipeline":
// navigateToPipeline(parsed.searchParams.get("id"));
// break;
// case "dashboard":
// navigateToDashboard();
// break;
// // ...
// }
// });
// Deep links are used in:
// - Discord message buttons (click → opens GooseFactory with context)
// - Email notifications
// - Mobile push notifications
// - Internal cross-references
6. Learning System Contracts
6.1 Feedback Event Flow
┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Modal │───▶│ McpApp │───▶│ API │───▶│ Validate │───▶│ Enrich │───▶│ Store │
│ iframe │ │ Host │ │ POST │ │ │ │ │ │ │
│ │ │(postMsg) │ │/v1/feed- │ │ Schema │ │ NLP │ │ JSONL │
│ │ │ │ │ back │ │ Dedup │ │ Themes │ │ Indexes │
│ │ │ │ │ │ │ Range │ │ Timing │ │ Aggs │
└──────────┘ └──────────┘ └───────────┘ └──────────┘ └──────────┘ └──────────┘
│
▼
┌──────────┐
│ Analyze │
│ │
│ Patterns │
│ Calibr. │
│ Rules │
└────┬─────┘
│
▼
┌──────────┐
│ Act │
│ │
│ Memory │
│ .hints │
│ Thresholds│
└──────────┘
6.2 Feedback Processing Pipeline Interfaces
// ═══════════════════════════════════════
// STAGE 1: Validate
// ═══════════════════════════════════════
interface ValidationResult {
valid: boolean;
event?: FeedbackEvent; // Present if valid
error?: {
type: "schema" | "orphaned" | "range" | "duplicate";
message: string;
details?: unknown;
};
}
// function validate(raw: unknown): ValidationResult
// ═══════════════════════════════════════
// STAGE 2: Enrich
// ═══════════════════════════════════════
interface EnrichedFeedbackEvent extends FeedbackEvent {
// Added by enrichment
_enriched: true;
_themes?: string[];
_sentiment?: number; // -1 to 1
_actionableItems?: string[];
_predictionDelta?: number; // actual - predicted
_pipelineContext?: {
stage: PipelineStage;
stagesCompleted: number;
totalDuration: number;
};
}
// function enrich(event: FeedbackEvent): EnrichedFeedbackEvent
// ═══════════════════════════════════════
// STAGE 3: Store
// ═══════════════════════════════════════
// Storage locations:
// feedback/raw/{YYYY-MM-DD}.jsonl — Append-only daily logs
// feedback/indexes/by-server-type.json
// feedback/indexes/by-dimension.json
// feedback/indexes/by-decision.json
// feedback/indexes/by-stage.json
// feedback/aggregates/weekly/{YYYY-Www}.json
// feedback/aggregates/monthly/{YYYY-MM}.json
// feedback/aggregates/rolling-30d.json
// function store(event: EnrichedFeedbackEvent): Promise<void>
// ═══════════════════════════════════════
// STAGE 4: Analyze (batch, runs periodically)
// ═══════════════════════════════════════
interface AnalysisResults {
// Pattern detection
approvalPatterns: ApprovalPatterns;
dimensionTrends: DimensionTrend[];
// Theme extraction
themes: ThemeCluster[];
// Calibration
calibration: CalibrationCurve;
// Anomalies
anomalies: Anomaly[];
// Candidate rules
candidateRules: CandidateRule[];
}
interface ApprovalPatterns {
overall: {
approvalRate: number;
totalDecisions: number;
avgTimeToDecisionMs: number;
};
byServerType: Record<string, {
approved: number;
rejected: number;
needsWork: number;
approvalRate: number;
}>;
byStage: Record<PipelineStage, {
approvalRate: number;
avgTimeMs: number;
count: number;
}>;
}
interface DimensionTrend {
dimension: QualityDimension;
avgScore: number;
trend: "improving" | "stable" | "declining";
trendDelta: number; // Change over period
dataPoints: number;
minimumThreshold?: number; // Learned rejection threshold
}
interface ThemeCluster {
theme: string; // e.g., "missing error handling"
occurrences: number;
avgSeverity: number;
lastSeen: string;
relatedDimensions: QualityDimension[];
exampleFeedback: string[]; // Quotes from feedback
}
interface Anomaly {
type: "approval_rate_drop" | "score_spike" | "new_rejection_pattern" | "calibration_drift";
description: string;
severity: "info" | "warning" | "critical";
data: Record<string, unknown>;
detectedAt: string;
}
interface CandidateRule {
rule: string; // Human-readable rule
promptInstruction: string; // How to inject into system prompt
confidence: number; // 0-1
occurrences: number;
source: "feedback_pattern" | "rejection_analysis" | "comparison" | "annotation";
domain: QualityDimension;
appliesTo: string[]; // Server types or ["all"]
}
// function analyze(events: EnrichedFeedbackEvent[]): AnalysisResults
// ═══════════════════════════════════════
// STAGE 5: Act (apply learnings)
// ═══════════════════════════════════════
interface ApplyResult {
memoryFilesUpdated: string[];
rulesAdded: CandidateRule[];
thresholdsChanged: Array<{
dimension: QualityDimension;
oldThreshold: number;
newThreshold: number;
}>;
autonomyLevelChanges: Array<{
taskType: string;
oldLevel: number;
newLevel: number;
}>;
}
// function applyLearnings(analysis: AnalysisResults): Promise<ApplyResult>
6.3 Memory File Schemas
# Memory file locations:
# ~/.config/goose/factory/memory/feedback-patterns.md
# ~/.config/goose/factory/memory/quality-standards.md
# ~/.config/goose/factory/memory/jake-preferences.md
# ~/.config/goose/factory/memory/improvement-log.md
// ═══════════════════════════════════════
// feedback-patterns.md structure
// ═══════════════════════════════════════
interface FeedbackPatternsFile {
lastUpdated: string;
basedOnEventCount: number;
sections: {
recurringThemes: Array<{
rank: number;
theme: string;
mentionCount: number;
description: string;
}>;
antiPatterns: Array<{
pattern: string;
rejectionRate: number; // 0-1
}>;
positivePatterns: Array<{
pattern: string;
avgScoreBonus: number;
}>;
};
}
// ═══════════════════════════════════════
// quality-standards.md structure
// ═══════════════════════════════════════
interface QualityStandardsFile {
lastCalibrated: string;
confidence: "low" | "medium" | "high"; // Based on data point count
dataPoints: number;
sections: {
minimumScores: Array<{ // Hard gates
dimension: QualityDimension;
minimum: number;
basedOn: number; // review count
avgScore: number;
}>;
targetScores: Array<{ // What "great" looks like
dimension: QualityDimension;
target: number;
topQuartile: number;
}>;
calibration: CalibrationCurve;
};
}
// ═══════════════════════════════════════
// jake-preferences.md structure
// ═══════════════════════════════════════
interface JakePreferencesFile {
lastUpdated: string;
sections: {
decisionPatterns: {
quickApproves: string; // What gets approved fast
deliberatesOn: string; // What takes time
autoRejects: string; // Instant rejections
};
communicationPreferences: {
preferredFormats: string[];
highEngagementTimes: string[];
lowEngagementTimes: string[];
};
designTaste: Record<string, string>;
codeStyle: Record<string, string>;
};
}
// ═══════════════════════════════════════
// improvement-log.md structure
// ═══════════════════════════════════════
interface ImprovementLogEntry {
date: string;
changes: Array<{
type: "rule_added" | "threshold_adjusted" | "calibration_updated" |
"format_changed" | "autonomy_level_changed";
description: string;
source: string; // What triggered this change
confidence: number;
}>;
}
6.4 Prediction API
// ═══════════════════════════════════════
// How the learning system exposes predictions
// Used by MCP server and UI to make smart decisions
// ═══════════════════════════════════════
// GET /v1/learning/predict
interface PredictionRequest {
workProductType: string;
mcpServerType?: string;
pipelineStage?: PipelineStage;
selfAssessedScores?: DimensionScore[];
rawConfidence?: number; // 0-1
}
interface PredictionResponse {
predictedScore: number; // 1-10
calibratedConfidence: number; // 0-1 (adjusted from raw)
predictedDecision: "approve" | "reject" | "needs_work";
decisionProbabilities: {
approve: number;
reject: number;
needsWork: number;
};
recommendedModal: string; // Best modal type for this review
riskFactors: string[]; // What might cause rejection
suggestedImprovements: string[];
}
// GET /v1/learning/autonomy/:taskType
interface AutonomyLevelResponse {
taskType: string;
currentLevel: number; // 0-4
approvalRate: number;
dataPoints: number;
nextLevelRequirements: {
approvalsNeeded: number;
minApprovalRate: number;
};
recentRegression: boolean;
}
// GET /v1/learning/stats
interface FeedbackStats {
totalEvents: number;
last30Days: {
approvalRate: number;
avgQualityScore: number;
avgTimeToDecisionMs: number;
predictionAccuracy: number; // ± delta
autoApproveRate: number;
feedbackVolume: number;
};
trends: {
approvalRate: "improving" | "stable" | "declining";
qualityScore: "improving" | "stable" | "declining";
decisionSpeed: "improving" | "stable" | "declining";
};
activeRules: number;
autonomyLevels: Record<string, number>;
}
6.5 Confidence Calibration Data Format
// ═══════════════════════════════════════
// Calibration maps raw AI confidence to
// actual observed approval rates.
// ═══════════════════════════════════════
interface CalibrationCurve {
lastUpdated: string;
totalDataPoints: number;
buckets: CalibrationBucket[];
// Overall calibration error (lower = better)
expectedCalibrationError: number; // 0-1
}
interface CalibrationBucket {
// Bucket range
rawConfidenceLow: number; // e.g., 0.90
rawConfidenceHigh: number; // e.g., 1.00
// Observed
avgPredictedConfidence: number;
avgActualApprovalRate: number;
sampleCount: number;
// Correction
correctionDelta: number; // actual - predicted (negative = overconfident)
calibratedConfidence: number; // What to use instead of raw
}
// Usage:
// rawConfidence = 0.92
// bucket = findBucket(rawConfidence) → bucket for 0.90-1.00
// calibrated = rawConfidence + bucket.correctionDelta → 0.92 + (-0.12) = 0.80
// Decision: 80% calibrated confidence → "educated guess" zone, standard review
7. Cross-Cutting Contracts
7.1 Error Handling Conventions
// ═══════════════════════════════════════
// Error Code Registry
// ═══════════════════════════════════════
// All error codes are documented. Every API response error
// uses one of the ErrorCode values from §1.3.
// Additional factory-specific error details:
interface FactoryErrorDetails {
// For CONFLICT errors:
currentState?: string; // Current entity state
requiredState?: string; // State needed for this operation
// For VALIDATION_ERROR:
validationErrors?: Array<{
field: string;
message: string;
received: unknown;
expected: string;
}>;
// For FORBIDDEN:
requiredScope?: Scope;
userScopes?: Scope[];
}
// ═══════════════════════════════════════
// Retry Policy
// ═══════════════════════════════════════
interface RetryPolicy {
// Client-side retry policy for API calls:
maxRetries: 3;
retryableStatusCodes: [408, 429, 500, 502, 503, 504];
backoff: {
type: "exponential";
baseDelayMs: 1000;
maxDelayMs: 30000;
jitterMs: 500; // Random jitter to prevent thundering herd
};
// For 429 (Rate Limited):
// Use Retry-After header if present
// Otherwise use standard backoff
// Non-retryable:
// 400, 401, 403, 404, 409, 410 — fix the request, don't retry
}
// ═══════════════════════════════════════
// Idempotency
// ═══════════════════════════════════════
// Mutating endpoints support idempotency via:
// Header: Idempotency-Key: <uuid>
//
// The server stores the response for 24h.
// If the same key is sent again, the stored response is returned.
// This prevents duplicate approvals, deployments, etc.
7.2 Logging Format
// ═══════════════════════════════════════
// Structured JSON Logging
// ═══════════════════════════════════════
interface LogEntry {
// Required fields
timestamp: string; // ISO 8601
level: "debug" | "info" | "warn" | "error" | "fatal";
message: string;
// Context
service: "api" | "mcp-server" | "learning" | "ws" | "worker";
requestId?: string;
userId?: string;
agentId?: string;
// Structured data
data?: Record<string, unknown>;
// Error info
error?: {
name: string;
message: string;
stack?: string;
code?: string;
};
// Performance
durationMs?: number;
// Tracing (OpenTelemetry)
traceId?: string;
spanId?: string;
}
// Example:
// {
// "timestamp": "2025-07-14T15:30:00.000Z",
// "level": "info",
// "message": "Task approved",
// "service": "api",
// "requestId": "req_abc123",
// "userId": "user_xyz",
// "data": {
// "taskId": "task_456",
// "pipelineId": "pipe_789",
// "decision": "approved",
// "durationMs": 45
// }
// }
// Log levels:
// debug → Development only, verbose trace info
// info → Normal operations: requests, state changes, events
// warn → Recoverable issues: retries, degraded performance, SLA warnings
// error → Failed operations: unhandled errors, integration failures
// fatal → System-level failures: cannot start, data corruption
7.3 Environment Variables
# ═══════════════════════════════════════
# API Server (packages/api)
# ═══════════════════════════════════════
FACTORY_API_PORT=3100
FACTORY_API_HOST=0.0.0.0
# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/goosefactory
DATABASE_POOL_SIZE=20
# Redis
REDIS_URL=redis://localhost:6379
# Auth
JWT_SECRET=<256-bit secret>
JWT_EXPIRY_SECONDS=900 # 15 minutes
REFRESH_TOKEN_EXPIRY_SECONDS=604800 # 7 days
# Storage
S3_ENDPOINT=https://your-r2.r2.cloudflarestorage.com
S3_BUCKET=goosefactory-assets
S3_ACCESS_KEY_ID=<key>
S3_SECRET_ACCESS_KEY=<secret>
# External integrations
DISCORD_BOT_TOKEN=<token>
DISCORD_GUILD_ID=<guild_id>
DISCORD_TASKS_CHANNEL_ID=<channel_id>
GITHUB_APP_ID=<app_id>
GITHUB_PRIVATE_KEY=<base64-encoded>
# Observability
LOG_LEVEL=info # debug | info | warn | error
OTEL_EXPORTER_ENDPOINT=https://otel.example.com
OTEL_SERVICE_NAME=goosefactory-api
# ═══════════════════════════════════════
# MCP Server (packages/mcp-server)
# ═══════════════════════════════════════
FACTORY_MCP_TRANSPORT=stdio # stdio | sse
FACTORY_API_URL=http://localhost:3100/v1
FACTORY_API_KEY=<internal-api-key>
# ═══════════════════════════════════════
# Learning System (packages/learning)
# ═══════════════════════════════════════
FEEDBACK_STORAGE_PATH=~/.config/goose/factory/feedback
MEMORY_FILES_PATH=~/.config/goose/factory/memory
ANALYSIS_INTERVAL_MINUTES=60 # Run analysis every hour
MIN_DATA_POINTS_FOR_RULES=5
CONFIDENCE_CALIBRATION_ENABLED=true
# ═══════════════════════════════════════
# Desktop App (packages/desktop)
# ═══════════════════════════════════════
FACTORY_API_URL=https://api.goosefactory.dev/v1
FACTORY_WS_URL=wss://api.goosefactory.dev/v1/ws
# GOOSE_PROVIDER, GOOSE_MODEL etc. are inherited from Goose
7.4 Monorepo File/Folder Structure
goosefactory/
├── CONTRACTS.md # ← THIS FILE (the source of truth)
├── README.md # Project overview
├── package.json # Monorepo root (workspaces)
├── turbo.json # Turborepo config (or nx.json)
├── tsconfig.base.json # Shared TypeScript config
├── .env.example # Template env vars
├── .gitignore
│
├── packages/
│ ├── shared/ # ← SHARED TYPES (generated from CONTRACTS.md)
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── src/
│ │ ├── types/
│ │ │ ├── pipeline.ts # Pipeline, PipelineStage, etc.
│ │ │ ├── task.ts # Task, TaskStatus, etc.
│ │ │ ├── approval.ts # Approval, ApprovalType, etc.
│ │ │ ├── agent.ts # Agent, AgentHealth, etc.
│ │ │ ├── asset.ts # Asset, AssetType, etc.
│ │ │ ├── feedback.ts # FeedbackEvent, FeedbackPayload, etc.
│ │ │ ├── modal.ts # ModalMessage, ModalResponse, etc.
│ │ │ ├── api.ts # ApiResponse, ApiError, PaginationParams
│ │ │ ├── ws.ts # WsEvent, WsChannel, event payloads
│ │ │ └── index.ts # Re-exports everything
│ │ ├── constants/
│ │ │ ├── stages.ts # KANBAN_STAGES, stage order
│ │ │ ├── errors.ts # ErrorCode enum values
│ │ │ └── sla.ts # Default SLA values
│ │ └── schemas/ # Zod schemas for runtime validation
│ │ ├── feedback.schema.ts
│ │ ├── pipeline.schema.ts
│ │ └── task.schema.ts
│ │
│ ├── api/ # API Engineer's domain
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── Dockerfile
│ │ └── src/
│ │ ├── server.ts
│ │ ├── routes/
│ │ ├── services/
│ │ ├── ws/
│ │ ├── middleware/
│ │ └── db/
│ │
│ ├── mcp-server/ # MCP Engineer's domain
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── src/
│ │ ├── index.ts
│ │ ├── tools/
│ │ ├── resources/
│ │ ├── prompts/
│ │ └── apps/
│ │
│ ├── modals/ # Modal Designer's domain
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── src/
│ │ ├── modals/ # 25 HTML modal files
│ │ │ ├── traffic-light.html
│ │ │ ├── tinder-swipe.html
│ │ │ └── ...
│ │ ├── shared/ # Common CSS/JS
│ │ │ ├── base-styles.css
│ │ │ ├── submit-handler.js
│ │ │ ├── timing-tracker.js
│ │ │ └── celebration-effects.js
│ │ └── registry.ts # Modal registrations
│ │
│ ├── learning/ # Learning Engineer's domain
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── src/
│ │ ├── pipeline/
│ │ ├── memory/
│ │ ├── calibration/
│ │ └── rules/
│ │
│ └── desktop/ # UI Engineer's domain (Goose fork)
│ ├── ... (forked Goose repo)
│ └── ui/desktop/src/
│ └── components/factory/ # Factory-specific UI components
│
└── infra/
├── db/
│ ├── migrations/
│ └── seed/
└── docker/
├── docker-compose.yml
├── docker-compose.dev.yml
├── Dockerfile.api
└── Dockerfile.learning
7.5 Shared Package Convention
// ═══════════════════════════════════════
// packages/shared is the SINGLE source of
// TypeScript types. All other packages import from it.
// ═══════════════════════════════════════
// In each package's package.json:
// "dependencies": {
// "@goosefactory/shared": "workspace:*"
// }
// Import convention:
// import type { Pipeline, Task, FeedbackEvent } from "@goosefactory/shared";
// import { KANBAN_STAGES, DEFAULT_SLA } from "@goosefactory/shared/constants";
// import { feedbackSchema } from "@goosefactory/shared/schemas";
// RULE: If you need a new type that crosses package boundaries,
// add it to @goosefactory/shared first, then use it.
// Never define the same type in two packages.
7.6 API Versioning & Deprecation
// ═══════════════════════════════════════
// Versioning rules
// ═══════════════════════════════════════
// 1. URL path versioning: /v1/, /v2/, etc.
// 2. Minor changes (new optional fields, new endpoints) → same version
// 3. Breaking changes (removed fields, changed types) → new version
// 4. Deprecated versions get a Sunset header with 90-day notice
// 5. Only 2 versions supported at any time (current + previous)
// Response headers on deprecated endpoints:
// Sunset: Sat, 14 Oct 2025 00:00:00 GMT
// Deprecation: true
// Link: <https://api.goosefactory.dev/v2/pipelines>; rel="successor-version"
7.7 Event Bus Contract
// ═══════════════════════════════════════
// Redis Streams event bus
// Used for decoupled async processing
// ═══════════════════════════════════════
// Stream names:
const REDIS_STREAMS = {
pipelines: "factory.pipelines",
tasks: "factory.tasks",
agents: "factory.agents",
deploys: "factory.deploys",
notifications: "factory.notifications",
feedback: "factory.feedback",
} as const;
// Consumer groups (each independently processes events):
const CONSUMER_GROUPS = {
wsBroadcaster: "ws-broadcaster", // Fans out to WebSocket clients
notificationWorker: "notification-worker",
discordBridge: "discord-bridge",
slaMonitor: "sla-monitor",
auditWriter: "audit-writer",
mcpResourceUpdater: "mcp-resource-updater",
feedbackProcessor: "feedback-processor",
} as const;
// Event format in Redis Stream:
interface RedisStreamEvent {
id: string; // Redis stream ID (timestamp-sequence)
type: WebSocketEventType;
timestamp: string;
data: string; // JSON-serialized payload
source: string; // Which service produced this
}
// Each consumer group processes events independently.
// Consumer group acknowledges events after processing (exactly-once within group).
// Events are retained for 7 days, then trimmed.
Appendix A: Stage Transition Matrix
FROM → TO | Trigger | Creates Task? | Validation
─────────────────────────────────────────────────────────────────────────────
intake → scaffolding | Pipeline created | No | None
scaffolding → building | Scaffold complete | No | Assets exist
building → testing | Build succeeds | No | Build artifact exists
testing → review | Tests pass | YES (review) | Coverage ≥ threshold
review → staging | Approval approved | YES (deploy) | Code review approved
staging → production | Staging tests pass | YES (deploy) | Staging deploy healthy
production → published | Production deploy OK | No | Deploy health check
ANY → failed | Unrecoverable error | YES (fix) | None
failed → {previous} | Manual retry | YES (approval)| None
Appendix B: SLA Escalation Timeline
T+0min Task created → Dashboard notification + Discord embed
T+30min Reminder #1 → Discord DM + dashboard badge pulse
T+2h Reminder #2 → Discord @mention + push notification
T+SLA-25% SLA Warning → Discord @here + sound alert in app
T+SLA SLA Breach → All channels + SMS (if configured)
T+SLA+2h Critical → Phone + auto-escalate to admin
Appendix C: Modal Type → Use Case Mapping
Modal Type | Use Case | Feedback Types Captured
────────────────────────────────────────────────────────────────────
traffic-light | binary_decision | decision, tags
tinder-swipe | batch_review | batchDecisions
report-card | multi_dimensional | scores, freeText
thermometer | subjective | temperature
spotlight | annotation | annotations
ranking-arena | comparison | ranking
speed-round | batch_review | batchDecisions
emoji-scale | subjective | decision (sentiment)
before-after | comparison | comparison
priority-poker | estimation | estimation
mission-briefing | high_stakes | decision, checklist, confidence
judges-scorecard | multi_dimensional | scores
quick-pulse | subjective | temperature (quick)
decision-tree | binary_decision | decision (branching)
hot-take | binary_decision | decision (timed)
side-by-side | comparison | comparison
checklist-ceremony | verification | checklist
confidence-meter | (paired w/ any) | confidence
voice-of-customer | subjective | scores, freeText
retrospective | retrospective | retrospective
slot-machine | subjective | scores (gamified)
mood-ring | subjective | custom (color/mood)
war-room | triage | batchDecisions
applause-meter | subjective | custom (tap count)
crystal-ball | estimation | estimation, custom
This document is the single source of truth for all GooseFactory inter-agent contracts. Every type defined here is canonical. If implementation differs from this document, the implementation is wrong.