From e38fb7be455b1e68377a0a1534111e803cdaeec0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 22:09:57 +0000 Subject: [PATCH] storybook: stop predicting the project in Jev task routing Remove the project choice question and the project list from the Jev request, along with its simulation, confidence row, and stabilizer rule. Project stays a plain manual picker. Project leads remain in agent descriptions because they help Jev choose the owner. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SWn6etZjfGz6p8NWYkCQve --- .../jev-task-creation/JevTaskComposer.tsx | 22 ++++------- .../prototypes/jev-task-creation/README.md | 15 +++++--- .../jev-task-creation/jev-classifier.ts | 37 +++---------------- .../stories/jev-task-creation.stories.tsx | 2 +- 4 files changed, 23 insertions(+), 53 deletions(-) diff --git a/ui/storybook/prototypes/jev-task-creation/JevTaskComposer.tsx b/ui/storybook/prototypes/jev-task-creation/JevTaskComposer.tsx index 29a5632f33e..d40c54ea05f 100644 --- a/ui/storybook/prototypes/jev-task-creation/JevTaskComposer.tsx +++ b/ui/storybook/prototypes/jev-task-creation/JevTaskComposer.tsx @@ -13,7 +13,6 @@ import { JEV_MIN_PROMPT_LENGTH, JEV_MODEL, JEV_NAME, - JEV_NO_PROJECT, JEV_PROJECTS, agentContextCoverage, stabilizeRouting, @@ -26,7 +25,7 @@ import { export type JevTaskComposerProps = { /** - * Both flows update mode, owner, and project live while you type. The title is + * Both flows update mode and owner live while you type. The title is * drafted once, when the task is started (Start task or Enter), like Claude * Code naming a session after the first message. * `suggest-first`: review the live suggestions, then create. @@ -173,7 +172,6 @@ export function JevTaskComposer({ const changed: Partial> = previous ? { workMode: previous.workMode !== next.workMode, assignee: previous.assigneeId !== next.assigneeId, - project: previous.projectId !== next.projectId, } : {}; routingRef.current = next; setRouting(next); @@ -241,7 +239,8 @@ export function JevTaskComposer({ const effective = useMemo(() => ({ title: overrides.title ?? suggestedTitle ?? "", assignee: overrides.assignee ?? routing?.assigneeId ?? null, - project: overrides.project !== undefined ? overrides.project : routing?.projectId ?? null, + // Project is not predicted; it stays a manual choice. + project: overrides.project ?? null, workMode: overrides.workMode ?? routing?.workMode ?? "standard", }), [overrides, routing, suggestedTitle]); @@ -517,7 +516,7 @@ function RoutingPanel({ return `${JEV_NAME} wasn't sure who should own this (${percent(routing!.confidence.assignee)}), so it goes to ${fallbackOwner.name}, ${why}.`; } if (status === "idle" && promptLength === 0 && !created) { - return `As you type, ${JEV_NAME} picks the mode, owner, and project. The title is written when you ${flow === "instant" ? "start" : "create"} the task.`; + return `As you type, ${JEV_NAME} picks the mode and owner. The title is written when you ${flow === "instant" ? "start" : "create"} the task.`; } if (status === "idle" && promptLength > 0 && promptLength < JEV_MIN_PROMPT_LENGTH) return "Keep going. Suggestions start once there's a bit more to go on."; return null; @@ -596,10 +595,8 @@ function RoutingPanel({ {JEV_PROJECTS.find((project) => project.id === effective.project)?.name ?? "No project"}} > {(close) => [ @@ -607,7 +604,6 @@ function RoutingPanel({ { setField("project", project.id); close(); }} > {project.name} @@ -616,7 +612,6 @@ function RoutingPanel({ { setField("project", null); close(); }} > No project @@ -652,7 +647,7 @@ function RoutingPanel({ ); } -const FIELD_LABELS: Record = { workMode: "Mode", assignee: "Assignee", project: "Project" }; +const FIELD_LABELS: Record = { workMode: "Mode", assignee: "Assignee" }; function choiceLabel(field: JevField, routing: JevRouting): string { if (field === "assignee") { @@ -661,12 +656,11 @@ function choiceLabel(field: JevField, routing: JevRouting): string { ? `${name(routing.assigneeId)} (fallback; ${JEV_NAME} leaned ${name(routing.jevAssigneeId)})` : name(routing.assigneeId); } - if (field === "project") return JEV_PROJECTS.find((project) => project.id === routing.projectId)?.name ?? "No project"; return workModeMetaFor(routing.workMode).label; } function ConfidenceDetails({ routing, prompt, agents, liveStats }: { routing: JevRouting; prompt: string; agents: typeof JEV_AGENTS; liveStats: LiveStats }) { - const fields: JevField[] = ["workMode", "assignee", "project"]; + const fields: JevField[] = ["workMode", "assignee"]; return (
diff --git a/ui/storybook/prototypes/jev-task-creation/README.md b/ui/storybook/prototypes/jev-task-creation/README.md index c7b2fa9cb90..9305bc04290 100644 --- a/ui/storybook/prototypes/jev-task-creation/README.md +++ b/ui/storybook/prototypes/jev-task-creation/README.md @@ -17,8 +17,8 @@ Two models do the work, because they are good at different things: - **[Jev](https://openrouter.ai/docs/guides/community/jev)** (`typesafe/jev-1.13`, TypeSafe's decision model on OpenRouter) answers typed `choice` questions for - the task's **mode**, which is its type (Auto, Plan, or Ask), plus its - **assignee** and **project**. It returns a choice, a + the task's **mode**, which is its type (Auto, Plan, or Ask), and its + **assignee**. It returns a choice, a confidence, and a probability for every option. It is fast and cheap: you pay for input tokens only, and output tokens are free. It does not generate text or explain its answers. @@ -27,7 +27,7 @@ Two models do the work, because they are good at different things: ## What the prototype shows -- **Live routing while you type.** Mode, assignee, and project re-route about +- **Live routing while you type.** Mode and assignee re-route about 250 ms after each pause in typing, and at least every 800 ms during continuous typing. Jev is fast and cheap enough for this: a call costs about $0.00004. - **Steady suggestions.** A chip only switches when Jev's new pick leads the @@ -41,6 +41,9 @@ Two models do the work, because they are good at different things: **Enter**, like Claude Code naming a session after the first message. It types into the header breadcrumb and you can click it to edit. Shift+Enter adds a new line. +- **Project is not predicted.** It stays a plain manual picker (default "No + project"). Project leads still appear in each agent's description, because + they help Jev pick the owner. - **Mode is the task's type** and uses the shipped `IssueWorkMode` values and labels: - **Auto** (`standard`): the agent does the work. @@ -74,8 +77,8 @@ Two models do the work, because they are good at different things: ## What's real and what's simulated `jev-classifier.ts` builds the real Decisions API request body -(`buildJevDecisionRequest`): the prompt, the company's agents and projects in -`state`, and three `choice` questions. The request and response types follow the +(`buildJevDecisionRequest`): the prompt and the company's agents in `state`, +and two `choice` questions. The request and response types follow the OpenRouter docs. Each agent's option in the assignee question is described by @@ -106,7 +109,7 @@ too (`draftTitle`). No API is called and no task is persisted. 1. Add a company-scoped server route, for example `POST /api/companies/:companyId/issues/suggest`. It builds the request from - the company's active agents and projects, leaving out paused agents and agents + the company's active agents and their context, leaving out paused agents and agents over budget. It calls `POST https://openrouter.ai/api/alpha/decisions` with the instance's OpenRouter key, and a small chat model for the title, in parallel. Keep the key on the server. diff --git a/ui/storybook/prototypes/jev-task-creation/jev-classifier.ts b/ui/storybook/prototypes/jev-task-creation/jev-classifier.ts index b22c042eb8a..4ac1f013f8b 100644 --- a/ui/storybook/prototypes/jev-task-creation/jev-classifier.ts +++ b/ui/storybook/prototypes/jev-task-creation/jev-classifier.ts @@ -8,8 +8,7 @@ import type { IssueWorkMode } from "@paperclipai/shared"; * does not generate text or explain itself. So routing is split in two: * * - Jev (`POST /api/alpha/decisions`, model `typesafe/jev-1.13`) answers `choice` - * questions for the task's mode (its type: Auto, Plan, or Ask), assignee, and - * project. + * questions for the task's mode (its type: Auto, Plan, or Ask) and assignee. * - A small text model drafts the title, the way Claude Code names a session. * * `buildJevDecisionRequest` produces the real request body. The response is @@ -191,7 +190,6 @@ export function agentContextCoverage(agents: JevAgent[], projects: JevProject[] }; } -export const JEV_NO_PROJECT = "none"; // --------------------------------------------------------------------------- // Request builder: this is what a server would send to OpenRouter. @@ -205,7 +203,6 @@ export function buildJevDecisionRequest(prompt: string, agents: JevAgent[] = JEV task_prompt: prompt, // Each agent's full description lives in the assignee criteria, so it is sent once. agents: available.map(({ id, name, title, reportsTo }) => ({ id, name, title, reports_to: reportsTo ?? "board" })), - projects: JEV_PROJECTS.map(({ id, name, description, leadAgentId }) => ({ id, name, description, lead_agent_id: leadAgentId })), }, questions: { work_mode: { @@ -222,14 +219,6 @@ export function buildJevDecisionRequest(prompt: string, agents: JevAgent[] = JEV instructions: "Which agent should own this task?", criteria: Object.fromEntries(available.map((agent) => [agent.id, describeAgentForJev(agent)])), }, - project: { - type: "choice", - instructions: "Which project does this task belong to?", - criteria: { - ...Object.fromEntries(JEV_PROJECTS.map((project) => [project.id, project.description])), - [JEV_NO_PROJECT]: "None of the projects fit.", - }, - }, }, }; } @@ -259,12 +248,6 @@ const AREA_OWNER: Record = { ops: "agent-darnold", }; -const PROJECT_KEYWORDS: Record = { - "project-board-ui": ["ui", "board", "dashboard", "page", "button", "dialog", "sidebar", "inbox", "screen", "mobile", "login", "storybook"], - "project-agent-runtime": ["agent", "runtime", "heartbeat", "adapter", "wake", "codex", "claude", "runner", "session"], - "project-budget-guardrails": ["budget", "spend", "cost", "billing", "limit", "invoice", "quota"], -}; - function score(text: string, keywords: string[]): number { return keywords.reduce((total, keyword) => total + (text.includes(keyword) ? 1 : 0), 0); } @@ -310,11 +293,6 @@ export function simulateJevDecisions(request: JevDecisionRequest): JevDecisionRe const assigneeTotal = Object.values(assigneeProbabilities).reduce((sum, value) => sum + value, 0); for (const id of Object.keys(assigneeProbabilities)) assigneeProbabilities[id]! /= assigneeTotal; - const projectScores: Record = { - ...Object.fromEntries(Object.entries(PROJECT_KEYWORDS).map(([id, keywords]) => [id, score(text, keywords)])), - [JEV_NO_PROJECT]: 0.5, - }; - const isQuestion = /\?\s*$/.test(prompt.trim()) || (/^(how|why|what|which|should|is|are|can|does)\b/.test(text) && !/^(can|could|would) you\b/.test(text)); const isLarge = /\b(plan|roadmap|migrate|migration|redesign|rewrite|overhaul|multi-step|phases?)\b/.test(text); const isInvestigation = /^(investigate|research|find out|figure out|look into|compare|evaluate)\b/.test(text); @@ -327,7 +305,6 @@ export function simulateJevDecisions(request: JevDecisionRequest): JevDecisionRe answers: { work_mode: choiceAnswer(softmax(modeScores)), assignee: choiceAnswer(assigneeProbabilities), - project: choiceAnswer(softmax(projectScores, 2.4)), }, // Output tokens are free on Jev; $0.042 per million input tokens. usage: { input_tokens: inputTokens, output_tokens: 0, cost: (inputTokens * 0.042) / 1_000_000 }, @@ -337,7 +314,7 @@ export function simulateJevDecisions(request: JevDecisionRequest): JevDecisionRe // --------------------------------------------------------------------------- // What the composer consumes -export type JevField = "workMode" | "assignee" | "project"; +export type JevField = "workMode" | "assignee"; /** * Who gets a task when Jev isn't confident about the owner: the org's first @@ -356,7 +333,6 @@ export type JevRouting = { assigneeSource: "jev" | "fallback"; /** Jev's own top pick, kept even when the fallback is used. */ jevAssigneeId: string; - projectId: string | null; confidence: Record; probabilities: Record>; /** Jev's likely owners other than the one assigned, offered when Jev is unsure. */ @@ -365,7 +341,7 @@ export type JevRouting = { }; export function routingFromJev(response: JevDecisionResponse, agents: JevAgent[] = JEV_AGENTS): JevRouting { - const { work_mode: mode, assignee, project } = response.answers as Record; + const { work_mode: mode, assignee } = response.answers as Record; const fallbackId = assignee!.confidence < JEV_CONFIDENCE_THRESHOLD ? fallbackAssigneeId(agents) : null; const assigneeId = fallbackId ?? assignee!.choice; const alternateAssigneeIds = Object.entries(assignee!.probabilities) @@ -377,9 +353,8 @@ export function routingFromJev(response: JevDecisionResponse, agents: JevAgent[] assigneeId, assigneeSource: fallbackId ? "fallback" : "jev", jevAssigneeId: assignee!.choice, - projectId: project!.choice === JEV_NO_PROJECT ? null : project!.choice, - confidence: { workMode: mode!.confidence, assignee: assignee!.confidence, project: project!.confidence }, - probabilities: { workMode: mode!.probabilities, assignee: assignee!.probabilities, project: project!.probabilities }, + confidence: { workMode: mode!.confidence, assignee: assignee!.confidence }, + probabilities: { workMode: mode!.probabilities, assignee: assignee!.probabilities }, alternateAssigneeIds, usage: response.usage, }; @@ -398,8 +373,6 @@ export function stabilizeRouting(previous: JevRouting | null, next: JevRouting): (next.probabilities[field][nextChoice] ?? 0) - (next.probabilities[field][previousChoice] ?? 0) < JEV_SWITCH_MARGIN; const result: JevRouting = { ...next }; if (keepPrevious("workMode", previous.workMode, next.workMode)) result.workMode = previous.workMode; - const projectKey = (id: string | null) => id ?? JEV_NO_PROJECT; - if (keepPrevious("project", projectKey(previous.projectId), projectKey(next.projectId))) result.projectId = previous.projectId; if (previous.assigneeSource === "jev" && next.assigneeSource === "jev" && keepPrevious("assignee", previous.assigneeId, next.assigneeId)) { result.assigneeId = previous.assigneeId; result.jevAssigneeId = previous.assigneeId; diff --git a/ui/storybook/stories/jev-task-creation.stories.tsx b/ui/storybook/stories/jev-task-creation.stories.tsx index 525efbe2f98..00a89499dd0 100644 --- a/ui/storybook/stories/jev-task-creation.stories.tsx +++ b/ui/storybook/stories/jev-task-creation.stories.tsx @@ -21,7 +21,7 @@ const meta = { description: { component: "Prototype: prompt-first task creation. You write what you need. Jev (`typesafe/jev-1.13`, TypeSafe's decision model on OpenRouter) answers " + - "`choice` questions for the task's mode, which is its type (Auto, Plan, or Ask), plus its assignee and project, each with a confidence and per-option probabilities. Mode, owner, and project update live as you type. Jev doesn't generate text, so a separate small text model drafts the title once, when the task is started with Start task or Enter, like Claude Code naming a session. " + + "`choice` questions for the task's mode, which is its type (Auto, Plan, or Ask), plus its assignee, each with a confidence and per-option probabilities. Mode and owner update live as you type; project is a manual choice. Jev doesn't generate text, so a separate small text model drafts the title once, when the task is started with Start task or Enter, like Claude Code naming a session. " + "Suggested values carry a sparkle. Anything you change is yours and is never overwritten; the undo icon restores the suggestion. " + "Below the confidence threshold the task goes to the org's first active agent that reports to the board (else its first active agent), and Jev's best guesses appear as one-click buttons. **Instant** stories create first; the title and routing arrive a moment later. " + "Jev's answers are simulated locally in the documented response shape (`prototypes/jev-task-creation/jev-classifier.ts`); **How sure Jev is → Request sent to Jev** shows the real request body. No API is called and no task is persisted.",