feat: Slice 1 polish - MiMo V2.5 model, visible reasoning, image attachments, mobile keyboard fix

- Switch conversation agent to xiaomi/mimo-v2.5 (multimodal: text + image)
- Render native reasoning parts as live 'Thinking trace' (streaming open,
  collapsed after completion); inline <think> extraction for streaming models
- Image attachments: picker (up to 4, 10MB each), base64 to Flue
  AgentPromptImage, authenticated blob-URL replay for historical images
- Mobile keyboard viewport fix: visual-viewport hook, fixed shell,
  interactive-widget=resizes-content, header pinned, composer follows keyboard
- Conversation to Signal to proposed Work: Convex persistence, Effect
  validation in @code/work-os, Work cards with exact source provenance
- Streamdown markdown + Mermaid chart rendering in chat messages
- Flue tool turns hidden, reasoning-containing turns remain visible
- Frontend regression tests: keyboard viewport, responsive shell,
  attachment overflow, authenticated images, reasoning traces, transforms
- .env.example updated to xiaomi/mimo-v2.5 config
This commit is contained in:
-Puter
2026-07-27 12:22:54 +05:30
parent 58ff7942bd
commit d8383a788e
55 changed files with 6487 additions and 2006 deletions

View File

@@ -17,6 +17,7 @@
"@code/backend": "workspace:*",
"@code/env": "workspace:*",
"@code/primitives": "workspace:*",
"@code/work-os": "workspace:*",
"@flue/runtime": "latest",
"@rivet-dev/agentos-core": "catalog:",
"convex": "catalog:",

View File

@@ -1,21 +1,13 @@
import path from "node:path";
import { parseAgentEnv } from "@code/env/agent";
import { defineAgent } from "@flue/runtime";
// import { agentos } from "../sandboxes/agentos";
import { local } from "@flue/runtime/node";
import paseo from "../skills/paseo/SKILL.md" with { type: "skill" };
import { paseoCli } from "../tools/paseo";
import { createSignalRoutingTools } from "../tools/signals";
import { createSliceOneTools } from "../tools/slice-one";
const repositoryRoot = path.resolve(process.cwd(), "../..");
const INSTRUCTIONS = `You are Zopu, the global planning and work-routing agent for the Zopu Work OS.
const INSTRUCTIONS = `You are Zopu for product Slice 1: Conversation to Signal to proposed Work.
## Your role
You listen to user messages in the persistent global conversation and decide whether they contain actionable work. The control plane stores every user message as exact evidence before you process it; you never supply or rewrite raw message text.
The application stores each user message as exact evidence before you process it. You may interpret that evidence, but never supply, rewrite, or invent source text.
## Work routing loop
@@ -23,7 +15,9 @@ When a user sends a message, follow this decision flow:
1. **Assess actionability.** Does the message contain a concrete problem, request, blocker, opportunity, or decision that warrants a work unit? Greetings, questions about the system, casual conversation, and exploration do NOT create work. If the message is casual conversation, respond naturally and do nothing else.
2. **Identify project context.** If the message references a project, call list_projects to find it. If the user has one project, use it. If the project is ambiguous, ask one focused clarification.
Direct questions, casual conversation, and image-reading requests must be answered immediately without calling any tools.
2. **Identify project context.** Call list_projects. If there is one project, use it. If there are several and the request is ambiguous, ask one focused question.
3. **Create a Signal (only when actionable).** When the message is actionable:
a. Call list_signal_evidence to see the exact admitted user messages.
@@ -31,46 +25,43 @@ When a user sends a message, follow this decision flow:
c. Call create_signal with a structured problem statement (title, summary, desiredOutcome, constraints). The problem statement must faithfully represent the user's own intent. Do not invent scope they did not mention.
d. Include the projectId when the project is known.
4. **Route the Signal.** After creating the Signal (or if one already exists):
a. Call list_active_issues for the relevant project.
b. Compare the Signal's problem to existing issues.
c. If the Signal clearly relates to an existing active issue, call attach_signal_to_issue.
d. If the Signal is new work that does not match any existing issue, call create_issue_from_signal.
4. **Route the Signal.** After creating the Signal:
a. Call list_proposed_work for the project.
b. If the Signal clearly describes the same desired outcome as existing Work, call attach_signal_to_work.
c. Otherwise call create_work_from_signal.
e. If genuinely uncertain whether to attach or create, ask one focused question.
5. **Explain the outcome.** Tell the user clearly what happened:
- "Created a Signal: [title] and attached it to issue #[number]: [issue title]."
- "Created a Signal: [title] and opened new issue #[number]: [issue title]."
- Include the problem statement title so the user can verify accuracy.
6. **Optionally begin work.** Only when the user explicitly asks to start or work on the issue, call begin_issue. Do not begin work automatically.
- "Captured [Signal title] and linked it to [Work title]."
- "Captured [Signal title] and proposed [Work title]."
- Keep the response brief; the product renders the durable Work card separately.
## Rules
- Never supply or rewrite the raw source message text. The control plane copies it server-side.
- Never create work from casual chat.
- You receive and can see images attached to the current user message. This model supports image input. Never claim images are unavailable, omitted, or unsupported. If a message has attached images, inspect them before responding.
- Never create Work from casual chat.
- Ask at most one focused clarification when genuinely ambiguous.
- Preserve project and organization scope at all times.
- Repeated delivery of the same message must not create duplicate Signals or attachments. The backend is idempotent.
- Never claim you created a Signal until the tool call returns successfully.
- Do not create Candidate Work, Work Units, or Runs directly. Those are downstream concerns.
- When the user asks about existing work, use list_active_issues and list_recent_signals to answer.`;
- Do not start implementation, planning, sandboxes, Git, verification, or delivery. Those are explicitly outside Slice 1.
- Proposed Work is the only Work status in this slice.`;
export { authenticatedAgentRoute as route } from "../auth";
export {
authenticatedAgentRoute as attachments,
authenticatedAgentRoute as route,
} from "../auth";
export default defineAgent(({ env, id }) => {
const { AGENT_MODEL_NAME, AGENT_MODEL_PROVIDER } = parseAgentEnv(env);
return {
cwd: repositoryRoot,
description:
"Project-scoped work-routing agent that turns conversation into Signals and routes them to issues.",
"Turns actionable conversation into provenanced Signals and proposed Work.",
instructions: INSTRUCTIONS,
model: `${AGENT_MODEL_PROVIDER}/${AGENT_MODEL_NAME}`,
// sandbox: agentos(),
sandbox: local({ cwd: repositoryRoot }),
skills: [paseo],
tools: [paseoCli, ...createSignalRoutingTools(id, env)],
thinkingLevel: "medium",
tools: createSliceOneTools(id, env),
};
});

View File

@@ -0,0 +1,145 @@
import { parseAgentEnv } from "@code/env/agent";
import { defineTool } from "@flue/runtime";
import { ConvexHttpClient } from "convex/browser";
import { makeFunctionReference } from "convex/server";
import * as v from "valibot";
const listProjectsRef = makeFunctionReference<
"query",
{ organizationId: string; token: string },
{ _id: string; name: string; description: string | null }[]
>("signalRouting:listProjects");
const listEvidenceRef = makeFunctionReference<
"query",
{ organizationId: string; token: string },
{ messageId: string; rawText: string; createdAt: number }[]
>("signalRouting:listEvidence");
const createSignalRef = makeFunctionReference<
"mutation",
{
organizationId: string;
projectId: string;
messageIds: string[];
problemStatement: {
title: string;
summary: string;
desiredOutcome: string;
constraints: string[];
};
processedByAgentInstanceId: string;
token: string;
},
{ signalId: string }
>("signalRouting:createSignal");
const listWorksRef = makeFunctionReference<
"query",
{ organizationId: string; projectId: string; token: string },
{ _id: string; title: string; objective: string; status: "proposed" }[]
>("works:listProposedForAgent");
const createWorkRef = makeFunctionReference<
"mutation",
{ organizationId: string; signalId: string; token: string },
{ created: boolean; workId: string }
>("works:createFromSignal");
const attachSignalRef = makeFunctionReference<
"mutation",
{ organizationId: string; signalId: string; workId: string; token: string },
{ attached: boolean; workId: string }
>("works:attachSignalToWork");
export const createSliceOneTools = (
organizationId: string,
runtimeEnv: Record<string, string | undefined>
) => {
const env = parseAgentEnv(runtimeEnv);
const client = new ConvexHttpClient(env.CONVEX_URL);
const token = env.FLUE_DB_TOKEN;
return [
defineTool({
description: "List the projects available in this organization.",
name: "list_projects",
async run() {
return await client.query(listProjectsRef, { organizationId, token });
},
}),
defineTool({
description:
"List exact admitted user messages that have not yet been used as Signal evidence.",
name: "list_signal_evidence",
async run() {
return await client.query(listEvidenceRef, { organizationId, token });
},
}),
defineTool({
description:
"Create one structured Signal from exact message IDs. Use only for actionable intent.",
input: v.object({
messageIds: v.array(v.string()),
problemStatement: v.object({
constraints: v.array(v.string()),
desiredOutcome: v.string(),
summary: v.string(),
title: v.string(),
}),
projectId: v.string(),
}),
name: "create_signal",
async run({ input }) {
return await client.mutation(createSignalRef, {
messageIds: input.messageIds,
organizationId,
problemStatement: input.problemStatement,
processedByAgentInstanceId: organizationId,
projectId: input.projectId,
token,
});
},
}),
defineTool({
description:
"List proposed Work for a project before deciding whether a new Signal belongs to existing Work.",
input: v.object({ projectId: v.string() }),
name: "list_proposed_work",
async run({ input }) {
return await client.query(listWorksRef, {
organizationId,
projectId: input.projectId,
token,
});
},
}),
defineTool({
description:
"Create one proposed Work from a Signal. This is idempotent for the same Signal.",
input: v.object({ signalId: v.string() }),
name: "create_work_from_signal",
async run({ input }) {
return await client.mutation(createWorkRef, {
organizationId,
signalId: input.signalId,
token,
});
},
}),
defineTool({
description:
"Attach a Signal to existing proposed Work when it describes the same desired outcome.",
input: v.object({ signalId: v.string(), workId: v.string() }),
name: "attach_signal_to_work",
async run({ input }) {
return await client.mutation(attachSignalRef, {
organizationId,
signalId: input.signalId,
token,
workId: input.workId,
});
},
}),
];
};

View File

@@ -30,6 +30,7 @@ import type * as signalRouting from "../signalRouting.js";
import type * as signals from "../signals.js";
import type * as todos from "../todos.js";
import type * as workflows from "../workflows.js";
import type * as works from "../works.js";
import type {
ApiFromModules,
@@ -60,6 +61,7 @@ declare const fullApi: ApiFromModules<{
signals: typeof signals;
todos: typeof todos;
workflows: typeof workflows;
works: typeof works;
}>;
/**

View File

@@ -282,6 +282,41 @@ export default defineSchema({
"messageId",
]),
works: defineTable({
organizationId: v.id("organizations"),
projectId: v.id("projects"),
title: v.string(),
objective: v.string(),
status: v.literal("proposed"),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("by_project_and_createdAt", ["projectId", "createdAt"])
.index("by_organization_and_createdAt", ["organizationId", "createdAt"]),
signalWorkAttachments: defineTable({
organizationId: v.id("organizations"),
projectId: v.id("projects"),
signalId: v.id("signals"),
workId: v.id("works"),
createdAt: v.number(),
})
.index("by_signal", ["signalId"])
.index("by_work", ["workId"])
.index("by_signal_and_work", ["signalId", "workId"]),
workEvents: defineTable({
organizationId: v.id("organizations"),
projectId: v.id("projects"),
workId: v.id("works"),
kind: v.union(v.literal("work.proposed"), v.literal("signal.attached")),
idempotencyKey: v.string(),
data: v.any(),
createdAt: v.number(),
})
.index("by_work_and_createdAt", ["workId", "createdAt"])
.index("by_work_and_idempotencyKey", ["workId", "idempotencyKey"]),
// -----------------------------------------------------------------
// Signal-to-issue attachments. A proper relation that links one Signal
// to one ProjectIssue, idempotent by (signalId, issueId). Multiple

View File

@@ -0,0 +1,224 @@
import { env } from "@code/env/convex";
import { convexTest } from "convex-test";
import { anyApi } from "convex/server";
import { describe, expect, test } from "vitest";
import { internal } from "./_generated/api";
import schema from "./schema";
declare global {
interface ImportMeta {
readonly glob: (pattern: string) => Record<string, () => Promise<unknown>>;
}
}
const modules = import.meta.glob("./**/*.ts");
const api = anyApi;
const identity = { tokenIdentifier: "https://convex.test|slice-one" };
describe("Slice 1 Work routing", () => {
test("creates one proposed Work and preserves exact source text", async () => {
const t = convexTest({ schema, modules });
const organization = await t
.withIdentity(identity)
.mutation(api.organizations.ensurePersonalOrganization, {});
const project = await t
.withIdentity(identity)
.mutation(internal.projects.persistPublicGitImport, {
remote: { defaultBranch: "main", documents: [], warnings: [] },
source: {
host: "github.com",
normalizedUrl: "https://github.com/example/slice-one",
projectName: "slice-one",
repositoryPath: "example/slice-one",
url: "https://github.com/example/slice-one",
},
userId: identity.tokenIdentifier,
});
const rawText = " Add a phone-ready Slice 1 experience. ";
const message = await t
.withIdentity(identity)
.mutation(api.conversationMessages.beginUserMessage, {
clientRequestId: "slice-one-request",
organizationId: organization._id,
rawText,
});
await t
.withIdentity(identity)
.mutation(api.conversationMessages.markAdmitted, {
clientRequestId: "slice-one-request",
organizationId: organization._id,
submissionId: "slice-one-submission",
});
const signal = await t.mutation(api.signalRouting.createSignal, {
messageIds: [message.messageId],
organizationId: organization._id,
problemStatement: {
constraints: ["Mobile web first"],
desiredOutcome: "The Slice 1 loop works on a phone.",
summary: "The current product is not ready for phone testing.",
title: "Make Slice 1 phone-ready",
},
processedByAgentInstanceId: organization._id,
projectId: project.id,
token: env.FLUE_DB_TOKEN,
});
const first = await t.mutation(api.works.createFromSignal, {
organizationId: organization._id,
signalId: signal.signalId,
token: env.FLUE_DB_TOKEN,
});
const repeated = await t.mutation(api.works.createFromSignal, {
organizationId: organization._id,
signalId: signal.signalId,
token: env.FLUE_DB_TOKEN,
});
expect(repeated).toEqual({ created: false, workId: first.workId });
const works = await t
.withIdentity(identity)
.query(api.works.listForProject, { projectId: project.id });
expect(works).toHaveLength(1);
expect(works[0]?.status).toBe("proposed");
expect(works[0]?.signals[0]?.sources[0]?.rawText).toBe(rawText);
});
test("keeps casual conversation out of Work", async () => {
const t = convexTest({ schema, modules });
const organization = await t
.withIdentity(identity)
.mutation(api.organizations.ensurePersonalOrganization, {});
const project = await t
.withIdentity(identity)
.mutation(internal.projects.persistPublicGitImport, {
remote: { defaultBranch: "main", documents: [], warnings: [] },
source: {
host: "git.openputer.com",
normalizedUrl: "https://git.openputer.com/puter/zopu-code",
projectName: "zopu-code",
repositoryPath: "puter/zopu-code",
url: "https://git.openputer.com/puter/zopu-code",
},
userId: identity.tokenIdentifier,
});
await t
.withIdentity(identity)
.mutation(api.conversationMessages.beginUserMessage, {
clientRequestId: "slice-one-casual",
organizationId: organization._id,
rawText: "Yo, how are you?",
});
await t
.withIdentity(identity)
.mutation(api.conversationMessages.markAdmitted, {
clientRequestId: "slice-one-casual",
organizationId: organization._id,
submissionId: "slice-one-casual-submission",
});
const works = await t
.withIdentity(identity)
.query(api.works.listForProject, { projectId: project.id });
expect(works).toEqual([]);
});
test("attaches another Signal to existing Work idempotently", async () => {
const t = convexTest({ schema, modules });
const organization = await t
.withIdentity(identity)
.mutation(api.organizations.ensurePersonalOrganization, {});
const project = await t
.withIdentity(identity)
.mutation(internal.projects.persistPublicGitImport, {
remote: { defaultBranch: "main", documents: [], warnings: [] },
source: {
host: "git.openputer.com",
normalizedUrl: "https://git.openputer.com/puter/zopu-code",
projectName: "zopu-code",
repositoryPath: "puter/zopu-code",
url: "https://git.openputer.com/puter/zopu-code",
},
userId: identity.tokenIdentifier,
});
const seedSignal = async (
clientRequestId: string,
rawText: string,
title: string
) => {
const message = await t
.withIdentity(identity)
.mutation(api.conversationMessages.beginUserMessage, {
clientRequestId,
organizationId: organization._id,
rawText,
});
await t
.withIdentity(identity)
.mutation(api.conversationMessages.markAdmitted, {
clientRequestId,
organizationId: organization._id,
submissionId: `${clientRequestId}-submission`,
});
return await t.mutation(api.signalRouting.createSignal, {
messageIds: [message.messageId],
organizationId: organization._id,
problemStatement: {
constraints: [],
desiredOutcome: "The Slice 1 phone flow is polished.",
summary: "The messages describe the same desired outcome.",
title,
},
processedByAgentInstanceId: organization._id,
projectId: project.id,
token: env.FLUE_DB_TOKEN,
});
};
const firstSignal = await seedSignal(
"slice-one-first",
"Polish the Slice 1 phone experience.",
"Polish Slice 1"
);
const secondSignal = await seedSignal(
"slice-one-second",
"Make the same Slice 1 experience easier to inspect on mobile.",
"Improve Slice 1 inspection"
);
const created = await t.mutation(api.works.createFromSignal, {
organizationId: organization._id,
signalId: firstSignal.signalId,
token: env.FLUE_DB_TOKEN,
});
const firstAttach = await t.mutation(api.works.attachSignalToWork, {
organizationId: organization._id,
signalId: secondSignal.signalId,
token: env.FLUE_DB_TOKEN,
workId: created.workId,
});
const repeatedAttach = await t.mutation(api.works.attachSignalToWork, {
organizationId: organization._id,
signalId: secondSignal.signalId,
token: env.FLUE_DB_TOKEN,
workId: created.workId,
});
expect(firstAttach).toEqual({ attached: true, workId: created.workId });
expect(repeatedAttach).toEqual({
attached: false,
workId: created.workId,
});
const works = await t
.withIdentity(identity)
.query(api.works.listForProject, { projectId: project.id });
expect(works).toHaveLength(1);
expect(works[0]?.signals).toHaveLength(2);
expect(
works[0]?.events.filter(
(event: { readonly kind: string }) => event.kind === "signal.attached"
)
).toHaveLength(1);
});
});

View File

@@ -0,0 +1,267 @@
import { env } from "@code/env/convex";
import {
signalAttachedEvent,
workDraftFromSignal,
workProposedEventFromSignal,
} from "@code/work-os";
import { ConvexError, v } from "convex/values";
import { Effect } from "effect";
import type { Doc, Id } from "./_generated/dataModel";
import { mutation, type MutationCtx, query } from "./_generated/server";
import { requireProjectMember } from "./authz";
const requireAgent = (token: string) => {
if (token !== env.FLUE_DB_TOKEN) {
throw new ConvexError("Invalid agent control token");
}
};
const requireSignal = async (
ctx: MutationCtx,
organizationId: Id<"organizations">,
signalId: Id<"signals">
): Promise<Doc<"signals">> => {
const signal = await ctx.db.get(signalId);
if (!signal || signal.organizationId !== organizationId) {
throw new ConvexError("Signal not found");
}
if (!signal.projectId) {
throw new ConvexError("Signal must belong to a project");
}
return signal;
};
const requireWork = async (
ctx: MutationCtx,
organizationId: Id<"organizations">,
workId: Id<"works">
): Promise<Doc<"works">> => {
const work = await ctx.db.get(workId);
if (!work || work.organizationId !== organizationId) {
throw new ConvexError("Work not found");
}
return work;
};
const attachSignal = async (
ctx: MutationCtx,
signal: Doc<"signals">,
work: Doc<"works">
): Promise<boolean> => {
const event = await Effect.runPromise(
signalAttachedEvent({
signal: {
organizationId: String(signal.organizationId),
projectId: String(signal.projectId),
signalId: String(signal._id),
title: signal.problemStatement.title,
},
work: {
organizationId: String(work.organizationId),
projectId: String(work.projectId),
workId: String(work._id),
},
})
).catch((error: unknown) => {
throw new ConvexError(
error instanceof Error ? error.message : "Invalid Work attachment"
);
});
const existing = await ctx.db
.query("signalWorkAttachments")
.withIndex("by_signal_and_work", (q) =>
q.eq("signalId", signal._id).eq("workId", work._id)
)
.unique();
if (existing) {
return false;
}
const createdAt = Date.now();
await ctx.db.insert("signalWorkAttachments", {
createdAt,
organizationId: work.organizationId,
projectId: work.projectId,
signalId: signal._id,
workId: work._id,
});
await ctx.db.insert("workEvents", {
createdAt,
data: event.data,
idempotencyKey: event.idempotencyKey,
kind: event.kind,
organizationId: work.organizationId,
projectId: work.projectId,
workId: work._id,
});
await ctx.db.patch(work._id, { updatedAt: createdAt });
return true;
};
export const listProposedForAgent = query({
args: {
organizationId: v.id("organizations"),
projectId: v.id("projects"),
token: v.string(),
},
handler: async (ctx, args) => {
requireAgent(args.token);
const project = await ctx.db.get(args.projectId);
if (!project || project.organizationId !== args.organizationId) {
throw new ConvexError("Project not found");
}
return await ctx.db
.query("works")
.withIndex("by_project_and_createdAt", (q) =>
q.eq("projectId", args.projectId)
)
.order("desc")
.take(50);
},
});
export const createFromSignal = mutation({
args: {
organizationId: v.id("organizations"),
signalId: v.id("signals"),
token: v.string(),
},
handler: async (ctx, args) => {
requireAgent(args.token);
const signal = await requireSignal(ctx, args.organizationId, args.signalId);
const existingAttachment = await ctx.db
.query("signalWorkAttachments")
.withIndex("by_signal", (q) => q.eq("signalId", signal._id))
.first();
if (existingAttachment) {
return { created: false, workId: existingAttachment.workId };
}
const draft = await Effect.runPromise(
workDraftFromSignal(signal.problemStatement)
).catch((error: unknown) => {
throw new ConvexError(
error instanceof Error ? error.message : "Invalid Work proposal"
);
});
const createdAt = Date.now();
const event = await Effect.runPromise(
workProposedEventFromSignal({
organizationId: String(signal.organizationId),
projectId: String(signal.projectId),
signalId: String(signal._id),
title: draft.title,
})
).catch((error: unknown) => {
throw new ConvexError(
error instanceof Error ? error.message : "Invalid Work event"
);
});
const workId = await ctx.db.insert("works", {
createdAt,
objective: draft.objective,
organizationId: signal.organizationId,
projectId: signal.projectId as Id<"projects">,
status: "proposed",
title: draft.title,
updatedAt: createdAt,
});
await ctx.db.insert("signalWorkAttachments", {
createdAt,
organizationId: signal.organizationId,
projectId: signal.projectId as Id<"projects">,
signalId: signal._id,
workId,
});
await ctx.db.insert("workEvents", {
createdAt,
data: event.data,
idempotencyKey: event.idempotencyKey,
kind: event.kind,
organizationId: signal.organizationId,
projectId: signal.projectId as Id<"projects">,
workId,
});
return { created: true, workId };
},
});
export const attachSignalToWork = mutation({
args: {
organizationId: v.id("organizations"),
signalId: v.id("signals"),
token: v.string(),
workId: v.id("works"),
},
handler: async (ctx, args) => {
requireAgent(args.token);
const signal = await requireSignal(ctx, args.organizationId, args.signalId);
const work = await requireWork(ctx, args.organizationId, args.workId);
return {
attached: await attachSignal(ctx, signal, work),
workId: work._id,
};
},
});
export const listForProject = query({
args: { projectId: v.id("projects") },
handler: async (ctx, args) => {
await requireProjectMember(ctx, args.projectId);
const works = await ctx.db
.query("works")
.withIndex("by_project_and_createdAt", (q) =>
q.eq("projectId", args.projectId)
)
.order("desc")
.take(100);
return await Promise.all(
works.map(async (work) => {
const attachments = await ctx.db
.query("signalWorkAttachments")
.withIndex("by_work", (q) => q.eq("workId", work._id))
.collect();
const signals = await Promise.all(
attachments.map(async (attachment) => {
const signal = await ctx.db.get(attachment.signalId);
if (!signal) {
return null;
}
const sources = await ctx.db
.query("signalSources")
.withIndex("by_signal_and_ordinal", (q) =>
q.eq("signalId", signal._id)
)
.collect();
return {
createdAt: signal.createdAt,
signalId: signal._id,
summary: signal.problemStatement.summary,
sources: sources
.sort((a, b) => a.ordinal - b.ordinal)
.map((source) => ({
createdAt: source.sourceCreatedAt,
messageId: source.messageId,
rawText: source.rawTextSnapshot,
submissionId: source.submissionId ?? null,
})),
title: signal.problemStatement.title,
};
})
);
const events = await ctx.db
.query("workEvents")
.withIndex("by_work_and_createdAt", (q) => q.eq("workId", work._id))
.order("desc")
.collect();
return {
...work,
events,
signals: signals.filter((signal) => signal !== null),
};
})
);
},
});

View File

@@ -15,6 +15,7 @@
"@better-auth/expo": "catalog:",
"@code/env": "workspace:*",
"@code/primitives": "workspace:*",
"@code/work-os": "workspace:*",
"@convex-dev/better-auth": "catalog:",
"better-auth": "catalog:",
"convex": "catalog:",
@@ -24,8 +25,8 @@
"devDependencies": {
"@code/config": "workspace:*",
"@types/node": "^24.3.0",
"typescript": "catalog:",
"convex-test": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
}
}

View File

@@ -0,0 +1,22 @@
{
"name": "@code/work-os",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": "./src/work.ts"
},
"scripts": {
"check-types": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
"effect": "catalog:"
},
"devDependencies": {
"@code/config": "workspace:*",
"@types/node": "^22.13.14",
"typescript": "catalog:",
"vitest": "catalog:"
}
}

View File

@@ -0,0 +1,134 @@
import { Effect } from "effect";
import { describe, expect, test } from "vitest";
import {
projectWorkNotices,
signalAttachedEvent,
workDraftFromSignal,
workProposedEventFromSignal,
} from "./work";
describe("workDraftFromSignal", () => {
test("creates a proposed Work draft from Signal meaning", async () => {
const draft = await Effect.runPromise(
workDraftFromSignal({
desiredOutcome: "The phone flow is usable.",
summary: "The current mobile workspace is hard to test.",
title: "Make Slice 1 phone-testable",
})
);
expect(draft).toEqual({
objective: "The phone flow is usable.",
title: "Make Slice 1 phone-testable",
});
});
test("rejects empty Signal meaning", async () => {
await expect(
Effect.runPromise(
workDraftFromSignal({ desiredOutcome: "", summary: "", title: "" })
)
).rejects.toMatchObject({ reason: "InvalidInput" });
});
test("constructs stable proposed and attached event drafts", async () => {
const proposed = await Effect.runPromise(
workProposedEventFromSignal({
organizationId: "org-1",
projectId: "project-1",
signalId: "signal-1",
title: " Improve the phone flow ",
})
);
const attached = await Effect.runPromise(
signalAttachedEvent({
signal: {
organizationId: "org-1",
projectId: "project-1",
signalId: "signal-2",
title: "Reuse the existing outcome",
},
work: {
organizationId: "org-1",
projectId: "project-1",
workId: "work-1",
},
})
);
expect(proposed).toEqual({
data: { signalId: "signal-1", title: "Improve the phone flow" },
idempotencyKey: "proposed:signal-1",
kind: "work.proposed",
});
expect(attached).toEqual({
data: { signalId: "signal-2", title: "Reuse the existing outcome" },
idempotencyKey: "signal:signal-2",
kind: "signal.attached",
});
});
test("rejects a cross-project attachment", async () => {
await expect(
Effect.runPromise(
signalAttachedEvent({
signal: {
organizationId: "org-1",
projectId: "project-2",
signalId: "signal-1",
title: "Cross project",
},
work: {
organizationId: "org-1",
projectId: "project-1",
workId: "work-1",
},
})
)
).rejects.toMatchObject({ reason: "ProjectMismatch" });
});
test("projects proposed Work events into chronological notices", () => {
expect(
projectWorkNotices([
{
_id: "work-2",
events: [{ _id: "event-2", createdAt: 20, kind: "work.proposed" }],
signals: [
{
sources: [{ messageId: "message-2", rawText: "Second" }],
},
],
},
{
_id: "work-1",
events: [
{ _id: "event-1", createdAt: 10, kind: "work.proposed" },
{ _id: "event-3", createdAt: 30, kind: "signal.attached" },
],
signals: [
{
sources: [{ messageId: "message-1", rawText: "First" }],
},
],
},
])
).toEqual([
{
createdAt: 10,
eventId: "event-1",
sourceMessageIds: ["message-1"],
sourceTexts: ["First"],
workId: "work-1",
},
{
createdAt: 20,
eventId: "event-2",
sourceMessageIds: ["message-2"],
sourceTexts: ["Second"],
workId: "work-2",
},
]);
});
});

View File

@@ -0,0 +1,231 @@
import { Array as EffectArray, Effect, Order, Schema } from "effect";
const MeaningfulString = Schema.String.check(
Schema.makeFilter((value) => value.trim().length > 0, {
expected: "a non-empty string",
})
);
export const WorkStatus = Schema.Literal("proposed");
export type WorkStatus = typeof WorkStatus.Type;
export const WorkDraft = Schema.Struct({
objective: MeaningfulString,
title: MeaningfulString,
});
export type WorkDraft = typeof WorkDraft.Type;
export const SignalProblem = Schema.Struct({
desiredOutcome: MeaningfulString,
summary: MeaningfulString,
title: MeaningfulString,
});
export const WorkCommandReason = Schema.Literals([
"InvalidInput",
"ProjectMismatch",
"TitleTooLong",
"ObjectiveTooLong",
]);
export type WorkCommandReason = typeof WorkCommandReason.Type;
export class WorkCommandError extends Schema.TaggedErrorClass<WorkCommandError>()(
"WorkCommandError",
{
message: Schema.String,
reason: WorkCommandReason,
}
) {}
export const workDraftFromSignal = Effect.fn("Work.draftFromSignal")(
function* workDraftFromSignal(input: unknown) {
const problem = yield* Schema.decodeUnknownEffect(SignalProblem)(
input
).pipe(
Effect.mapError(
() =>
new WorkCommandError({
message: "Signal is missing a valid problem statement",
reason: "InvalidInput",
})
)
);
const title = problem.title.trim();
const objective = problem.desiredOutcome.trim();
if (title.length > 160) {
return yield* new WorkCommandError({
message: "Work title must be 160 characters or fewer",
reason: "TitleTooLong",
});
}
if (objective.length > 2000) {
return yield* new WorkCommandError({
message: "Work objective must be 2000 characters or fewer",
reason: "ObjectiveTooLong",
});
}
return { objective, title } satisfies WorkDraft;
}
);
const WorkIdentity = Schema.Struct({
organizationId: MeaningfulString,
projectId: MeaningfulString,
workId: MeaningfulString,
});
const SignalIdentity = Schema.Struct({
organizationId: MeaningfulString,
projectId: MeaningfulString,
signalId: MeaningfulString,
title: MeaningfulString,
});
const WorkAttachmentInput = Schema.Struct({
signal: SignalIdentity,
work: WorkIdentity,
});
export const WorkEventKind = Schema.Literals([
"work.proposed",
"signal.attached",
]);
export type WorkEventKind = typeof WorkEventKind.Type;
export const WorkEventDraft = Schema.Struct({
data: Schema.Struct({
signalId: MeaningfulString,
title: MeaningfulString,
}),
idempotencyKey: MeaningfulString,
kind: WorkEventKind,
});
export type WorkEventDraft = typeof WorkEventDraft.Type;
export const workProposedEventFromSignal = Effect.fn(
"Work.proposedEventFromSignal"
)(function* workProposedEventFromSignal(input: unknown) {
const signal = yield* Schema.decodeUnknownEffect(SignalIdentity)(input).pipe(
Effect.mapError(
() =>
new WorkCommandError({
message: "Signal identity is invalid",
reason: "InvalidInput",
})
)
);
return {
data: { signalId: signal.signalId, title: signal.title.trim() },
idempotencyKey: `proposed:${signal.signalId}`,
kind: "work.proposed",
} satisfies WorkEventDraft;
});
export const signalAttachedEvent = Effect.fn("Work.signalAttachedEvent")(
function* signalAttachedEvent(input: unknown) {
const attachment = yield* Schema.decodeUnknownEffect(WorkAttachmentInput)(
input
).pipe(
Effect.mapError(
() =>
new WorkCommandError({
message: "Work attachment input is invalid",
reason: "InvalidInput",
})
)
);
if (
attachment.signal.organizationId !== attachment.work.organizationId ||
attachment.signal.projectId !== attachment.work.projectId
) {
return yield* new WorkCommandError({
message: "Signal and Work must belong to the same project",
reason: "ProjectMismatch",
});
}
return {
data: {
signalId: attachment.signal.signalId,
title: attachment.signal.title.trim(),
},
idempotencyKey: `signal:${attachment.signal.signalId}`,
kind: "signal.attached",
} satisfies WorkEventDraft;
}
);
const WorkNoticeInput = Schema.Array(
Schema.Struct({
_id: MeaningfulString,
events: Schema.Array(
Schema.Struct({
_id: MeaningfulString,
createdAt: Schema.Number,
kind: WorkEventKind,
})
),
signals: Schema.Array(
Schema.Struct({
sources: Schema.Array(
Schema.Struct({ messageId: MeaningfulString, rawText: Schema.String })
),
})
),
})
);
export interface WorkNotice {
readonly createdAt: number;
readonly eventId: string;
readonly sourceMessageIds: readonly string[];
readonly sourceTexts: readonly string[];
readonly workId: string;
}
const projectWorkNoticesEffect = Effect.fn("Work.projectNotices")(
function* projectWorkNoticesEffect(input: unknown) {
const works = yield* Schema.decodeUnknownEffect(WorkNoticeInput)(
input
).pipe(
Effect.mapError(
() =>
new WorkCommandError({
message: "Work notice data is invalid",
reason: "InvalidInput",
})
)
);
const notices = works.flatMap((work) => {
const sourceMessageIds = work.signals.flatMap((signal) =>
signal.sources.map((source) => source.messageId)
);
const sourceTexts = work.signals.flatMap((signal) =>
signal.sources.map((source) => source.rawText)
);
return work.events
.filter((event) => event.kind === "work.proposed")
.map(
(event): WorkNotice => ({
createdAt: event.createdAt,
eventId: event._id,
sourceMessageIds,
sourceTexts,
workId: work._id,
})
);
});
return EffectArray.sortWith(
notices,
(notice) => notice.createdAt,
Order.Number
);
}
);
export const projectWorkNotices = (input: unknown): readonly WorkNotice[] =>
Effect.runSync(projectWorkNoticesEffect(input));

View File

@@ -0,0 +1,8 @@
{
"extends": "@code/config/tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}