/** * Response envelope — PROTOCOL §9.3. * * The reply to a request/response command (§5.1). `command` echoes the command type being * responded to; `id` correlates with the command's `id`. `success:false` carries an * `error: { code, message, data? }` from the §8 registry. Events never carry a correlation * `id` — `response` is the only server→client direction that does (§5.1). */ import { Type, type Static } from '@sinclair/typebox'; import { ErrorCodeSchema, type ErrorCode } from '@/protocol/error-codes'; const WebClientSchema = Type.Object({ id: Type.String({ minLength: 1 }), name: Type.Optional(Type.String()), createdAt: Type.Integer({ minimum: 0 }), lastSeenAt: Type.Optional(Type.Integer({ minimum: 0 })), revokedAt: Type.Optional(Type.Integer({ minimum: 0 })), }, { additionalProperties: false }); export const WebApproveResponseSchema = Type.Object({}, { additionalProperties: false }); export const WebTokenCreateResponseSchema = Type.Object({ setupToken: Type.String({ minLength: 1 }) }, { additionalProperties: false }); export const WebClientsResponseSchema = Type.Object({ clients: Type.Array(WebClientSchema) }, { additionalProperties: false }); export const WebRevokeResponseSchema = Type.Object({}, { additionalProperties: false }); export const WebTokenRotateResponseSchema = Type.Object({ token: Type.String({ minLength: 1 }) }, { additionalProperties: false }); // ─── Workspace response payloads (saga j8dtju.6, protocol v1 additive) ─────────── // Worktree cwd is intentionally exposed for CLI/web rendering, alongside the derived // `repository_cwd` (the repository's main-checkout root — grouped/labeling display path // for the worktree's ORIGINAL project; the repository common-dir/git-dir paths proper // remain omitted). export const WorkspaceSummarySchema = Type.Object({ workspace_id: Type.String({ minLength: 1 }), label: Type.String({ minLength: 1 }), cwd: Type.String({ minLength: 1 }), ownership: Type.Union([Type.Literal('user'), Type.Literal('managed')]), lifecycle: Type.Union([ Type.Literal('active'), Type.Literal('retiring'), Type.Literal('retired'), Type.Literal('conflict'), ]), repository_fingerprint: Type.String({ minLength: 1 }), /** Canonical main-checkout root of the repository this worktree belongs to (optional * additive v1 field; bare repositories project their root). Lets clients group and * label sibling worktrees by their original project. */ repository_cwd: Type.Optional(Type.String({ minLength: 1 })), worktree_fingerprint: Type.String({ minLength: 1 }), branch: Type.Optional(Type.String()), integration_target: Type.Optional(Type.String()), created_at: Type.Integer({ minimum: 0 }), updated_at: Type.Integer({ minimum: 0 }), binding_count: Type.Integer({ minimum: 0 }), live_agent_count: Type.Integer({ minimum: 0 }), live_session_count: Type.Integer({ minimum: 0 }), }, { additionalProperties: false }); export const WorkspaceBindingSummarySchema = Type.Object({ agent_id: Type.String({ minLength: 1 }), session_id: Type.String({ minLength: 1 }), bound_at: Type.Integer({ minimum: 0 }), }, { additionalProperties: false }); export const WorkspaceDetailSchema = Type.Object({ summary: WorkspaceSummarySchema, last_head_commit: Type.Optional(Type.String()), last_probed_at: Type.Optional(Type.Integer({ minimum: 0 })), tombstone: Type.Optional(Type.Object({ retired_at: Type.Integer({ minimum: 0 }), reason: Type.String({ minLength: 1 }), }, { additionalProperties: false })), conflict: Type.Optional(Type.Object({ detected_at: Type.Integer({ minimum: 0 }), reason: Type.String({ minLength: 1 }), }, { additionalProperties: false })), bindings: Type.Array(WorkspaceBindingSummarySchema), }, { additionalProperties: false }); export const WorkspaceCreateResponseSchema = Type.Object({ workspace: WorkspaceDetailSchema, agent_id: Type.String({ minLength: 1 }), session_id: Type.String({ minLength: 1 }), }, { additionalProperties: false }); export const WorkspaceAttachResponseSchema = Type.Object({ workspace: WorkspaceDetailSchema, agent_id: Type.String({ minLength: 1 }), session_id: Type.String({ minLength: 1 }), }, { additionalProperties: false }); export const WorkspaceListResponseSchema = Type.Object({ workspaces: Type.Array(WorkspaceSummarySchema) }, { additionalProperties: false }); export const WorkspaceStatusResponseSchema = WorkspaceDetailSchema; export const WorkspaceRetireResponseSchema = Type.Object({ workspace: WorkspaceDetailSchema, branch_deleted: Type.Boolean() }, { additionalProperties: false }); /** Structured refusal returned when retirement cannot safely proceed. */ export const WorkspaceRetireRefusalSchema = Type.Object({ kind: Type.Union([Type.Literal('confirmation_required'), Type.Literal('refused')]), reason: Type.String({ minLength: 1 }), workspace_id: Type.String({ minLength: 1 }), requested_disposition: Type.Optional(Type.Union([Type.Literal('delete_if_merged'), Type.Literal('keep'), Type.Literal('delete_confirmed')])), confirmation_disposition: Type.Optional(Type.Literal('delete_confirmed')), }, { additionalProperties: false }); export type WorkspaceSummary = Static; export type WorkspaceDetail = Static; export type WorkspaceCreateResponse = Static; export type WorkspaceAttachResponse = Static; export type WorkspaceListResponse = Static; export type WorkspaceStatusResponse = Static; export type WorkspaceRetireResponse = Static; export type WorkspaceRetireRefusal = Static; export const WorkspaceResponseSchemas = { 'workspace.create': WorkspaceCreateResponseSchema, 'workspace.attach': WorkspaceAttachResponseSchema, 'workspace.list': WorkspaceListResponseSchema, 'workspace.status': WorkspaceStatusResponseSchema, 'workspace.retire': WorkspaceRetireResponseSchema, } as const; export const WebAdminResponseSchemas = { 'web.approve': WebApproveResponseSchema, 'web.token.create': WebTokenCreateResponseSchema, 'web.clients': WebClientsResponseSchema, 'web.revoke': WebRevokeResponseSchema, 'web.token.rotate': WebTokenRotateResponseSchema, } as const; export type WebAdminResponse = | Static | Static | Static | Static | Static; /** The error object on an unsuccessful response (§9.3, §8 codes). */ export const ResponseErrorSchema = Type.Object({ code: ErrorCodeSchema, message: Type.String(), data: Type.Optional(Type.Unknown()), }); /** The response envelope (§9.3). */ export const ResponseSchema = Type.Object({ type: Type.Literal('response'), /** Correlates with the command's `id` (§5.1). */ id: Type.String(), /** Echoes the command type being responded to (§9.3). */ command: Type.String(), success: Type.Boolean(), data: Type.Optional(Type.Unknown()), error: Type.Optional(ResponseErrorSchema), }); export type Response = Static; export type ResponseError = Static; /** * Build a successful response. Omit `data` entirely when undefined (exactOptionalPropertyTypes * — never pass `undefined` explicitly). */ export function ok(id: string, command: string, data?: unknown): Response { const res: Response = { type: 'response', id, command, success: true }; if (data !== undefined) res.data = data; return res; } /** Build an error response. Omit `error.data` when undefined. */ export function err( id: string, command: string, code: ErrorCode, message: string, data?: unknown, ): Response { const error: ResponseError = { code, message }; if (data !== undefined) error.data = data; return { type: 'response', id, command, success: false, error }; }