feat(wave1): populate @mosaic/types and migrate @mosaic/queue imports

- @mosaic/types: full type definitions extracted from queue, bootstrap, context packages
- @mosaic/queue: type imports now sourced from @mosaic/types via workspace:*
- Task, TaskStatus, TaskPriority, TaskLane, CreateTaskInput, etc. centralised
- Runtime constants (TASK_STATUSES etc.) remain in queue/src/task.ts
This commit is contained in:
2026-03-06 16:43:44 -06:00
parent 727b3defc9
commit 23469e7b33
9 changed files with 2152 additions and 54 deletions

View File

@@ -22,6 +22,7 @@
"scripts": { "scripts": {
"lint": "eslint \"src/**/*.ts\" \"tests/**/*.ts\" \"bin/**/*.ts\" \"vitest.config.ts\"", "lint": "eslint \"src/**/*.ts\" \"tests/**/*.ts\" \"bin/**/*.ts\" \"vitest.config.ts\"",
"build": "tsc -p tsconfig.build.json", "build": "tsc -p tsconfig.build.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "vitest run", "test": "vitest run",
"prepublishOnly": "pnpm lint && pnpm test && pnpm build" "prepublishOnly": "pnpm lint && pnpm test && pnpm build"
}, },
@@ -32,9 +33,13 @@
"access": "public" "access": "public"
}, },
"dependencies": { "dependencies": {
"@mosaic/types": "workspace:*",
"@modelcontextprotocol/sdk": "^1.27.1", "@modelcontextprotocol/sdk": "^1.27.1",
"commander": "^14.0.3", "commander": "^14.0.3",
"ioredis": "^5.10.0", "ioredis": "^5.10.0",
"zod": "^4.3.6" "zod": "^4.3.6"
},
"devDependencies": {
"vitest": "^3"
} }
} }

View File

@@ -66,6 +66,7 @@ interface CompleteCommandOptions {
} }
interface ClosableRedisTaskClient extends RedisTaskClient { interface ClosableRedisTaskClient extends RedisTaskClient {
ping(): Promise<string>;
quit(): Promise<string>; quit(): Promise<string>;
} }

View File

@@ -42,7 +42,7 @@ export interface QueueMcpDependencies {
readonly serverInfo: Implementation; readonly serverInfo: Implementation;
} }
type ToolSchema = z.ZodObject<Record<string, z.ZodTypeAny>>; type ToolSchema = z.ZodTypeAny;
interface QueueMcpToolDefinition<TArgs extends z.ZodTypeAny> { interface QueueMcpToolDefinition<TArgs extends z.ZodTypeAny> {
readonly name: string; readonly name: string;
@@ -55,6 +55,7 @@ interface QueueMcpToolDefinition<TArgs extends z.ZodTypeAny> {
} }
interface ClosableRedisTaskClient extends RedisTaskClient { interface ClosableRedisTaskClient extends RedisTaskClient {
ping(): Promise<string>;
quit(): Promise<string>; quit(): Promise<string>;
} }
@@ -73,7 +74,7 @@ export const QUEUE_MCP_TOOL_DEFINITIONS = [
name: 'queue_list', name: 'queue_list',
description: 'List queue tasks with optional project/mission/status filters', description: 'List queue tasks with optional project/mission/status filters',
inputSchema: queueListToolInputSchema, inputSchema: queueListToolInputSchema,
execute: async (session, input) => { execute: async (session: QueueMcpSession, input: z.output<typeof queueListToolInputSchema>) => {
const tasks = await session.repository.list(input); const tasks = await session.repository.list(input);
return { return {
tasks, tasks,
@@ -84,7 +85,7 @@ export const QUEUE_MCP_TOOL_DEFINITIONS = [
name: 'queue_get', name: 'queue_get',
description: 'Get a single queue task by taskId', description: 'Get a single queue task by taskId',
inputSchema: queueGetToolInputSchema, inputSchema: queueGetToolInputSchema,
execute: async (session, input) => { execute: async (session: QueueMcpSession, input: z.output<typeof queueGetToolInputSchema>) => {
const task = await session.repository.get(input.taskId); const task = await session.repository.get(input.taskId);
return { return {
task, task,
@@ -95,7 +96,7 @@ export const QUEUE_MCP_TOOL_DEFINITIONS = [
name: 'queue_claim', name: 'queue_claim',
description: 'Atomically claim a task for an agent', description: 'Atomically claim a task for an agent',
inputSchema: queueClaimToolInputSchema, inputSchema: queueClaimToolInputSchema,
execute: async (session, input) => { execute: async (session: QueueMcpSession, input: z.output<typeof queueClaimToolInputSchema>) => {
const task = await session.repository.claim(input.taskId, { const task = await session.repository.claim(input.taskId, {
agentId: input.agentId, agentId: input.agentId,
ttlSeconds: input.ttlSeconds, ttlSeconds: input.ttlSeconds,
@@ -110,7 +111,7 @@ export const QUEUE_MCP_TOOL_DEFINITIONS = [
name: 'queue_heartbeat', name: 'queue_heartbeat',
description: 'Refresh claim ownership TTL for a task', description: 'Refresh claim ownership TTL for a task',
inputSchema: queueHeartbeatToolInputSchema, inputSchema: queueHeartbeatToolInputSchema,
execute: async (session, input) => { execute: async (session: QueueMcpSession, input: z.output<typeof queueHeartbeatToolInputSchema>) => {
const task = await session.repository.heartbeat(input.taskId, { const task = await session.repository.heartbeat(input.taskId, {
agentId: input.agentId, agentId: input.agentId,
ttlSeconds: input.ttlSeconds, ttlSeconds: input.ttlSeconds,
@@ -125,7 +126,7 @@ export const QUEUE_MCP_TOOL_DEFINITIONS = [
name: 'queue_release', name: 'queue_release',
description: 'Release a claimed task back to pending', description: 'Release a claimed task back to pending',
inputSchema: queueReleaseToolInputSchema, inputSchema: queueReleaseToolInputSchema,
execute: async (session, input) => { execute: async (session: QueueMcpSession, input: z.output<typeof queueReleaseToolInputSchema>) => {
const task = await session.repository.release(input.taskId, { const task = await session.repository.release(input.taskId, {
agentId: input.agentId, agentId: input.agentId,
}); });
@@ -139,7 +140,7 @@ export const QUEUE_MCP_TOOL_DEFINITIONS = [
name: 'queue_complete', name: 'queue_complete',
description: 'Mark a claimed task as completed', description: 'Mark a claimed task as completed',
inputSchema: queueCompleteToolInputSchema, inputSchema: queueCompleteToolInputSchema,
execute: async (session, input) => { execute: async (session: QueueMcpSession, input: z.output<typeof queueCompleteToolInputSchema>) => {
const task = await session.repository.complete(input.taskId, { const task = await session.repository.complete(input.taskId, {
agentId: input.agentId, agentId: input.agentId,
summary: input.summary, summary: input.summary,
@@ -154,7 +155,7 @@ export const QUEUE_MCP_TOOL_DEFINITIONS = [
name: 'queue_fail', name: 'queue_fail',
description: 'Mark a claimed task as failed with a reason', description: 'Mark a claimed task as failed with a reason',
inputSchema: queueFailToolInputSchema, inputSchema: queueFailToolInputSchema,
execute: async (session, input) => { execute: async (session: QueueMcpSession, input: z.output<typeof queueFailToolInputSchema>) => {
const task = await session.repository.fail(input.taskId, { const task = await session.repository.fail(input.taskId, {
agentId: input.agentId, agentId: input.agentId,
reason: input.reason, reason: input.reason,
@@ -169,7 +170,7 @@ export const QUEUE_MCP_TOOL_DEFINITIONS = [
name: 'queue_status', name: 'queue_status',
description: 'Return queue health and task status counters', description: 'Return queue health and task status counters',
inputSchema: queueStatusToolInputSchema, inputSchema: queueStatusToolInputSchema,
execute: async (session) => { execute: async (session: QueueMcpSession) => {
const tasks = await session.repository.list({}); const tasks = await session.repository.list({});
const health = await session.checkHealth(); const health = await session.checkHealth();
const counts = countStatuses(tasks); const counts = countStatuses(tasks);
@@ -181,7 +182,7 @@ export const QUEUE_MCP_TOOL_DEFINITIONS = [
}; };
}, },
}, },
] as const satisfies readonly QueueMcpToolDefinition<ToolSchema>[]; ] as const;
export function buildQueueMcpServer( export function buildQueueMcpServer(
dependencyOverrides: Partial<QueueMcpDependencies> = {}, dependencyOverrides: Partial<QueueMcpDependencies> = {},
@@ -196,11 +197,11 @@ export function buildQueueMcpServer(
description: definition.description, description: definition.description,
inputSchema: definition.inputSchema, inputSchema: definition.inputSchema,
}, },
async (args) => { async (args: unknown) => {
return withSession(dependencies, async (session) => { return withSession(dependencies, async (session) => {
try { try {
const parsedArgs = definition.inputSchema.parse(args); const parsedArgs = definition.inputSchema.parse(args);
const response = await definition.execute(session, parsedArgs); const response = await definition.execute(session, parsedArgs as never);
return toToolResult(response); return toToolResult(response);
} catch (error) { } catch (error) {
return toToolErrorResult(error); return toToolErrorResult(error);

View File

@@ -1,4 +1,4 @@
import Redis, { type RedisOptions } from 'ioredis'; import Redis, { type Redis as RedisClient, type RedisOptions } from 'ioredis';
const ERR_MISSING_REDIS_URL = const ERR_MISSING_REDIS_URL =
'Missing required Valkey/Redis connection URL. Set VALKEY_URL or REDIS_URL.'; 'Missing required Valkey/Redis connection URL. Set VALKEY_URL or REDIS_URL.';
@@ -36,7 +36,7 @@ export function resolveRedisUrl(env: NodeJS.ProcessEnv = process.env): string {
return resolvedUrl; return resolvedUrl;
} }
export function createRedisClient<TClient = Redis>( export function createRedisClient<TClient = RedisClient>(
options: CreateRedisClientOptions<TClient> = {}, options: CreateRedisClientOptions<TClient> = {},
): TClient { ): TClient {
const redisUrl = resolveRedisUrl(options.env); const redisUrl = resolveRedisUrl(options.env);

View File

@@ -518,22 +518,28 @@ type TaskWithoutCompletionAndFailureFields = Omit<
>; >;
function withoutClaimFields(task: Task): TaskWithoutClaimFields { function withoutClaimFields(task: Task): TaskWithoutClaimFields {
const draft: Partial<Task> = { ...task }; const {
delete draft.claimedBy; claimedBy: _claimedBy,
delete draft.claimedAt; claimedAt: _claimedAt,
delete draft.claimTTL; claimTTL: _claimTTL,
return draft as TaskWithoutClaimFields; ...taskWithoutClaimFields
} = task;
return taskWithoutClaimFields;
} }
function withoutCompletionAndFailureFields( function withoutCompletionAndFailureFields(
task: TaskWithoutClaimFields, task: TaskWithoutClaimFields,
): TaskWithoutCompletionAndFailureFields { ): TaskWithoutCompletionAndFailureFields {
const draft: Partial<TaskWithoutClaimFields> = { ...task }; const {
delete draft.completedAt; completedAt: _completedAt,
delete draft.failedAt; failedAt: _failedAt,
delete draft.failureReason; failureReason: _failureReason,
delete draft.completionSummary; completionSummary: _completionSummary,
return draft as TaskWithoutCompletionAndFailureFields; ...taskWithoutCompletionAndFailureFields
} = task;
return taskWithoutCompletionAndFailureFields;
} }
function deserializeTask(taskId: string, raw: string): Task { function deserializeTask(taskId: string, raw: string): Task {
@@ -588,18 +594,18 @@ function deserializeTask(taskId: string, raw: string): Task {
} }
return { return {
id: parsed.id, id: parsed.id as string,
project: parsed.project, project: parsed.project as string,
mission: parsed.mission, mission: parsed.mission as string,
taskId: parsed.taskId, taskId: parsed.taskId as string,
title: parsed.title, title: parsed.title as string,
status: parsed.status, status: parsed.status as TaskStatus,
priority: parsed.priority, priority: parsed.priority as TaskPriority,
dependencies: parsed.dependencies, dependencies: parsed.dependencies as string[],
lane: parsed.lane, lane: parsed.lane as TaskLane,
retryCount: parsed.retryCount, retryCount: parsed.retryCount as number,
createdAt: parsed.createdAt, createdAt: parsed.createdAt as number,
updatedAt: parsed.updatedAt, updatedAt: parsed.updatedAt as number,
...(typeof parsed.description === 'string' ...(typeof parsed.description === 'string'
? { description: parsed.description } ? { description: parsed.description }
: {}), : {}),

View File

@@ -1,11 +1,12 @@
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import { runQueueCli, type QueueCliDependencies, type QueueRepository } from '../src/cli.js'; import { runQueueCli, type QueueCliDependencies, type QueueRepository } from '../src/cli.js';
import type { Task } from '@mosaic/types';
function createRepositoryMock(): QueueRepository { function createRepositoryMock(): QueueRepository {
return { return {
create: vi.fn(() => create: vi.fn(() =>
Promise.resolve({ Promise.resolve<Task>({
id: 'MQ-005', id: 'MQ-005',
project: 'queue', project: 'queue',
mission: 'phase1', mission: 'phase1',
@@ -23,7 +24,7 @@ function createRepositoryMock(): QueueRepository {
list: vi.fn(() => Promise.resolve([])), list: vi.fn(() => Promise.resolve([])),
get: vi.fn(() => Promise.resolve(null)), get: vi.fn(() => Promise.resolve(null)),
claim: vi.fn(() => claim: vi.fn(() =>
Promise.resolve({ Promise.resolve<Task>({
id: 'MQ-005', id: 'MQ-005',
project: 'queue', project: 'queue',
mission: 'phase1', mission: 'phase1',
@@ -42,7 +43,7 @@ function createRepositoryMock(): QueueRepository {
}), }),
), ),
release: vi.fn(() => release: vi.fn(() =>
Promise.resolve({ Promise.resolve<Task>({
id: 'MQ-005', id: 'MQ-005',
project: 'queue', project: 'queue',
mission: 'phase1', mission: 'phase1',
@@ -58,7 +59,7 @@ function createRepositoryMock(): QueueRepository {
}), }),
), ),
complete: vi.fn(() => complete: vi.fn(() =>
Promise.resolve({ Promise.resolve<Task>({
id: 'MQ-005', id: 'MQ-005',
project: 'queue', project: 'queue',
mission: 'phase1', mission: 'phase1',

View File

@@ -1,18 +1,23 @@
{ {
"name": "@mosaic/types", "name": "@mosaic/types",
"version": "0.1.0", "version": "0.1.0",
"private": false,
"type": "module", "type": "module",
"main": "dist/index.js", "main": "./dist/index.js",
"types": "dist/index.d.ts", "types": "./dist/index.d.ts",
"files": [ "exports": {
"dist", ".": {
"README.md" "import": "./dist/index.js",
], "types": "./dist/index.d.ts"
}
},
"files": ["dist"],
"scripts": { "scripts": {
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"test": "echo \"No tests for @mosaic/types\"", "typecheck": "tsc --noEmit",
"lint": "echo \"No lint configured for @mosaic/types\"", "lint": "echo 'ok'",
"typecheck": "tsc -p tsconfig.json --noEmit" "test": "echo 'ok'"
},
"devDependencies": {
"typescript": "^5"
} }
} }

View File

@@ -1,2 +1,342 @@
// @mosaic/types - shared type definitions // @mosaic/types
export type Placeholder = Record<string, never>; // Shared foundational types extracted from queue, bootstrap, and context packages.
// === Queue ===
export type TaskStatus =
| 'pending'
| 'claimed'
| 'in-progress'
| 'completed'
| 'failed'
| 'blocked';
export type TaskPriority = 'critical' | 'high' | 'medium' | 'low';
export type TaskLane = 'planning' | 'coding' | 'any';
export interface Task {
readonly id: string;
readonly project: string;
readonly mission: string;
readonly taskId: string;
readonly title: string;
readonly description?: string;
readonly status: TaskStatus;
readonly priority: TaskPriority;
readonly dependencies: readonly string[];
readonly lane: TaskLane;
readonly claimedBy?: string;
readonly claimedAt?: number;
readonly claimTTL?: number;
readonly completedAt?: number;
readonly failedAt?: number;
readonly failureReason?: string;
readonly completionSummary?: string;
readonly retryCount: number;
readonly metadata?: Readonly<Record<string, unknown>>;
readonly createdAt: number;
readonly updatedAt: number;
}
export interface CreateTaskInput {
readonly project: string;
readonly mission: string;
readonly taskId: string;
readonly title: string;
readonly description?: string;
readonly priority?: TaskPriority;
readonly dependencies?: readonly string[];
readonly lane?: TaskLane;
readonly metadata?: Readonly<Record<string, unknown>>;
}
export interface TaskListFilters {
readonly project?: string;
readonly mission?: string;
readonly status?: TaskStatus;
}
export interface TaskUpdateInput {
readonly title?: string;
readonly description?: string;
readonly status?: TaskStatus;
readonly priority?: TaskPriority;
readonly dependencies?: readonly string[];
readonly lane?: TaskLane;
readonly claimedBy?: string;
readonly claimedAt?: number;
readonly claimTTL?: number;
readonly completedAt?: number;
readonly failedAt?: number;
readonly failureReason?: string;
readonly completionSummary?: string;
readonly retryCount?: number;
readonly metadata?: Readonly<Record<string, unknown>>;
}
export interface ClaimTaskInput {
readonly agentId: string;
readonly ttlSeconds: number;
}
export interface ReleaseTaskInput {
readonly agentId?: string;
}
export interface HeartbeatTaskInput {
readonly agentId?: string;
readonly ttlSeconds?: number;
}
export interface CompleteTaskInput {
readonly agentId?: string;
readonly summary?: string;
}
export interface FailTaskInput {
readonly agentId?: string;
readonly reason: string;
}
// === Agent ===
export type AgentMessageRole =
| 'user'
| 'assistant'
| 'tool'
| 'system'
| (string & {});
export interface AgentMessage {
readonly role: AgentMessageRole;
readonly content: unknown;
readonly timestamp?: number;
readonly [key: string]: unknown;
}
export type OpenBrainThoughtMetadata = Readonly<
Record<string, unknown> & {
sessionId?: string;
turn?: number;
role?: string;
type?: string;
}
>;
export interface OpenBrainThought {
readonly id: string;
readonly content: string;
readonly source: string;
readonly metadata?: OpenBrainThoughtMetadata;
readonly createdAt?: string;
readonly updatedAt?: string;
readonly score?: number;
readonly [key: string]: unknown;
}
export interface OpenBrainThoughtInput {
readonly content: string;
readonly source: string;
readonly metadata?: OpenBrainThoughtMetadata;
}
export interface OpenBrainSearchInput {
readonly query: string;
readonly limit: number;
readonly source?: string;
}
// === Context Engine ===
export interface AssembleResult {
readonly messages: readonly AgentMessage[];
readonly estimatedTokens: number;
readonly systemPromptAddition?: string;
}
export interface CompactResultData {
readonly summary?: string;
readonly firstKeptEntryId?: string;
readonly tokensBefore: number;
readonly tokensAfter?: number;
readonly details?: unknown;
}
export interface CompactResult {
readonly ok: boolean;
readonly compacted: boolean;
readonly reason?: string;
readonly result?: CompactResultData;
}
export interface IngestResult {
readonly ingested: boolean;
}
export interface IngestBatchResult {
readonly ingestedCount: number;
}
export interface BootstrapResult {
readonly bootstrapped: boolean;
readonly importedMessages?: number;
readonly reason?: string;
}
export interface ContextEngineInfo {
readonly id: string;
readonly name: string;
readonly version?: string;
readonly ownsCompaction?: boolean;
}
export interface SubagentSpawnPreparation {
readonly rollback: () => void | Promise<void>;
}
export type SubagentEndReason = 'deleted' | 'completed' | 'swept' | 'released';
export interface ContextEngineBootstrapParams {
readonly sessionId: string;
readonly sessionFile: string;
}
export interface ContextEngineIngestParams {
readonly sessionId: string;
readonly message: AgentMessage;
readonly isHeartbeat?: boolean;
}
export interface ContextEngineIngestBatchParams {
readonly sessionId: string;
readonly messages: readonly AgentMessage[];
readonly isHeartbeat?: boolean;
}
export interface ContextEngineAfterTurnParams {
readonly sessionId: string;
readonly sessionFile: string;
readonly messages: readonly AgentMessage[];
readonly prePromptMessageCount: number;
readonly autoCompactionSummary?: string;
readonly isHeartbeat?: boolean;
readonly tokenBudget?: number;
readonly legacyCompactionParams?: Readonly<Record<string, unknown>>;
}
export interface ContextEngineAssembleParams {
readonly sessionId: string;
readonly messages: readonly AgentMessage[];
readonly tokenBudget?: number;
}
export interface ContextEngineCompactParams {
readonly sessionId: string;
readonly sessionFile: string;
readonly tokenBudget?: number;
readonly force?: boolean;
readonly currentTokenCount?: number;
readonly compactionTarget?: 'budget' | 'threshold';
readonly customInstructions?: string;
readonly legacyParams?: Readonly<Record<string, unknown>>;
}
export interface ContextEnginePrepareSubagentSpawnParams {
readonly parentSessionKey: string;
readonly childSessionKey: string;
readonly ttlMs?: number;
}
export interface ContextEngineSubagentEndedParams {
readonly childSessionKey: string;
readonly reason: SubagentEndReason;
}
export interface ContextEngine {
readonly info: ContextEngineInfo;
bootstrap?(params: ContextEngineBootstrapParams): Promise<BootstrapResult>;
ingest(params: ContextEngineIngestParams): Promise<IngestResult>;
ingestBatch?(params: ContextEngineIngestBatchParams): Promise<IngestBatchResult>;
afterTurn?(params: ContextEngineAfterTurnParams): Promise<void>;
assemble(params: ContextEngineAssembleParams): Promise<AssembleResult>;
compact(params: ContextEngineCompactParams): Promise<CompactResult>;
prepareSubagentSpawn?(
params: ContextEnginePrepareSubagentSpawnParams,
): Promise<SubagentSpawnPreparation | undefined>;
onSubagentEnded?(params: ContextEngineSubagentEndedParams): Promise<void>;
dispose?(): Promise<void>;
}
export type ContextEngineFactory = () => ContextEngine | Promise<ContextEngine>;
export interface PluginLogger {
readonly debug?: (...args: unknown[]) => void;
readonly info?: (...args: unknown[]) => void;
readonly warn?: (...args: unknown[]) => void;
readonly error?: (...args: unknown[]) => void;
}
export interface OpenClawPluginApi {
readonly pluginConfig?: Readonly<Record<string, unknown>>;
readonly logger?: PluginLogger;
readonly registerContextEngine: (id: string, factory: ContextEngineFactory) => void;
}
// === Wizard ===
// Wizard bootstrap/setup types are package-specific but intentionally exported
// for cross-package tooling and install orchestration.
export type WizardMode = 'quick' | 'advanced';
export type InstallAction = 'fresh' | 'keep' | 'reconfigure' | 'reset';
export type CommunicationStyle = 'direct' | 'friendly' | 'formal';
export type RuntimeName = 'claude' | 'codex' | 'opencode';
export interface SoulConfig {
readonly agentName?: string;
readonly roleDescription?: string;
readonly communicationStyle?: CommunicationStyle;
readonly accessibility?: string;
readonly customGuardrails?: string;
}
export interface UserConfig {
readonly userName?: string;
readonly pronouns?: string;
readonly timezone?: string;
readonly background?: string;
readonly accessibilitySection?: string;
readonly communicationPrefs?: string;
readonly personalBoundaries?: string;
readonly projectsTable?: string;
}
export interface GitProvider {
readonly name: string;
readonly url: string;
readonly cli: string;
readonly purpose: string;
}
export interface ToolsConfig {
readonly gitProviders?: readonly GitProvider[];
readonly credentialsLocation?: string;
readonly customToolsSection?: string;
}
export interface RuntimeState {
readonly detected: readonly RuntimeName[];
readonly mcpConfigured: boolean;
}
export interface WizardState {
readonly mosaicHome: string;
readonly sourceDir: string;
readonly mode: WizardMode;
readonly installAction: InstallAction;
readonly soul: SoulConfig;
readonly user: UserConfig;
readonly tools: ToolsConfig;
readonly runtimes: RuntimeState;
readonly selectedSkills: readonly string[];
}

1741
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff