From 61e85dc5cfbadb1f81c54e734b4853052b19c555 Mon Sep 17 00:00:00 2001 From: "Brian J. Fox" Date: Sun, 8 Nov 2026 13:46:43 +0000 Subject: [PATCH 1/3] fix(hub): let the person's ask decide how many designs start design_app treated the project's Design concepts preference as a hard cap on the concepts at work, so when the person asked the coordinator for five looks with the preference at three, the hub refused with too_many_concepts and the coordinator told them to change Settings, which cannot reach a running project. The preference now guides the coordinator only when the person names no number, and the tool refuses only above the ceiling the preference offers (five), counting designers already started. The preference guidance, the design_app description and the artifact list's note on preferenceGuidance all say that what the person says in the conversation wins. --- apps/hub/src/artifacts/tools.ts | 2 +- apps/hub/src/config/preferences.ts | 30 +++++++++---------- apps/hub/src/projects/deploy-tools.test.ts | 34 ++++++++++++---------- apps/hub/src/projects/deploy-tools.ts | 14 +++++---- 4 files changed, 42 insertions(+), 38 deletions(-) diff --git a/apps/hub/src/artifacts/tools.ts b/apps/hub/src/artifacts/tools.ts index 1066d5e5..54fdabed 100644 --- a/apps/hub/src/artifacts/tools.ts +++ b/apps/hub/src/artifacts/tools.ts @@ -44,7 +44,7 @@ const ToolRead = ArtifactRead.merge({ "part?": "number.integer > 0" }); const readDefinition = { name: "read_artifact", description: - "See which project documents exist, or read one.\nWith no arguments it lists: each document's artifactId, kind, title, current version, sha256, approvedVersion, the version a person approved, and openQuestionCount, the questions its current version leaves open for the person; inFlight, the writer, reviewer, coder and designer runs still open, each with its status, deployed until its first assignment arrives and running after, and each working (mid-turn) or waiting (parked for your next mail), with progress once it has acted: its turns so far, its latest tool calls as tool and target, the files it has written, and lastActivityAt; failed, the latest runs that stopped on an error, with when and why, which deliver nothing more; awaitingApproval, the kinds of filed documents whose current version waits on the person's approval; and preferenceGuidance, how the person has said they want to work.\nWith a kind or an artifactId it returns that document's content at its current version, or at the given version, with the openQuestions that version leaves for the person; a long document comes in numbered parts, and part reads the next one.", + "See which project documents exist, or read one.\nWith no arguments it lists: each document's artifactId, kind, title, current version, sha256, approvedVersion, the version a person approved, and openQuestionCount, the questions its current version leaves open for the person; inFlight, the writer, reviewer, coder and designer runs still open, each with its status, deployed until its first assignment arrives and running after, and each working (mid-turn) or waiting (parked for your next mail), with progress once it has acted: its turns so far, its latest tool calls as tool and target, the files it has written, and lastActivityAt; failed, the latest runs that stopped on an error, with when and why, which deliver nothing more; awaitingApproval, the kinds of filed documents whose current version waits on the person's approval; and preferenceGuidance, how the person said they wanted to work when the project started; what they say in the conversation wins over it.\nWith a kind or an artifactId it returns that document's content at its current version, or at the given version, with the openQuestions that version leaves for the person; a long document comes in numbered parts, and part reads the next one.", inputSchema: { ...ToolRead.toJsonSchema() }, }; const briefWriteDefinition = { diff --git a/apps/hub/src/config/preferences.ts b/apps/hub/src/config/preferences.ts index d90f9d35..10cfbe0f 100644 --- a/apps/hub/src/config/preferences.ts +++ b/apps/hub/src/config/preferences.ts @@ -106,30 +106,35 @@ export const PREFERENCES: readonly PreferenceEntry[] = [ hint: "How many different looks the designers make when you explore several.", topic: "Design concepts", options: [ - { id: "one", label: "One", guidance: "Start one design concept." }, + { + id: "one", + label: "One", + guidance: + "Start one design concept, unless the person asks for more.", + }, { id: "two", label: "Two", guidance: - "When the person wants to explore several looks, start two different design concepts.", + "When the person wants to explore several looks and names no number, start two different design concepts.", }, { id: "three", label: "Three", guidance: - "When the person wants to explore several looks, start three different design concepts.", + "When the person wants to explore several looks and names no number, start three different design concepts.", }, { id: "four", label: "Four", guidance: - "When the person wants to explore several looks, start four different design concepts.", + "When the person wants to explore several looks and names no number, start four different design concepts.", }, { id: "five", label: "Five", guidance: - "When the person wants to explore several looks, start five different design concepts.", + "When the person wants to explore several looks and names no number, start five different design concepts.", }, ], default: "three", @@ -349,19 +354,14 @@ export function preferenceGuidance( ); } -/** How many design concepts the designers make at once; the options are listed in count order. */ -export function designConceptCount(choices: Preferences): number { +/** The most design concepts at work at once: the largest count the preference offers. What the + * person asks for in the conversation decides how many start, up to this, so the preference is + * guidance for the coordinator rather than a limit here. */ +export function designConceptCeiling(): number { const entry = PREFERENCES.find( (candidate) => candidate.id === "design_concepts", )!; - const index = entry.options.findIndex( - (option) => option.id === choices[entry.id], - ); - return ( - (index < 0 - ? entry.options.findIndex((option) => option.id === entry.default) - : index) + 1 - ); + return entry.options.length; } /** Whether the project's coder adds new packages without a person's approval. */ diff --git a/apps/hub/src/projects/deploy-tools.test.ts b/apps/hub/src/projects/deploy-tools.test.ts index 618d036c..ce64be8d 100644 --- a/apps/hub/src/projects/deploy-tools.test.ts +++ b/apps/hub/src/projects/deploy-tools.test.ts @@ -119,7 +119,7 @@ test("the coder spawns only from the approved current build plan version", async } }); -test("design_app starts one designer per concept within the person's limit, each named before its assignment", async () => { +test("design_app starts one designer per concept the person asks for, each named before its assignment", async () => { const events: string[] = []; const sha = (n: number) => String(n).repeat(64); const artifacts = [ @@ -136,6 +136,7 @@ test("design_app starts one designer per concept within the person's limit, each sha256: sha(1), })); let runs = 0; + let inFlight: Record[] = []; const fetch = async (path: string, init: RequestInit) => { const body = init.body ? JSON.parse(String(init.body)) : undefined; if (path.endsWith("/workflows/deployments")) { @@ -155,7 +156,7 @@ test("design_app starts one designer per concept within the person's limit, each if (path.endsWith("/artifacts") && init.method === "GET") return Response.json({ artifacts, - inFlight: [], + inFlight, preferences: { design_concepts: "two" }, }); return Response.json({ code: "unexpected_route" }, { status: 404 }); @@ -188,31 +189,32 @@ test("design_app starts one designer per concept within the person's limit, each const instruction = "Design the dashboard the requirements describe, in the person's words."; const lookIdeas = "Calm and readable, like a paper notebook."; - const concepts = ["Sleek", "Pastel", "Retro"].map((label) => ({ - label, - direction: label + " look", - })); + const concept = (label: string) => ({ label, direction: label + " look" }); + const concepts = ["Sleek", "Pastel", "Retro"].map(concept); + // Only the ceiling on concepts at work holds; the person's two is guidance for the coordinator. + inFlight = ["Calm", "Bold", "Editorial"].map((label, index) => ({ + runId: "live-" + String(index), + role: "designer", + status: "running", + concept: label, + })); expect( (await run("design_app", { instruction, lookIdeas, concepts })).content, - ).toStartWith("too_many_concepts:"); + ).toStartWith("too_many_concepts: at most 5"); + inFlight = []; + // The person asked for three looks while their preference says two: what they say wins. expect( - ( - await run("design_app", { - instruction, - lookIdeas, - concepts: concepts.slice(0, 2), - }) - ).isError, + (await run("design_app", { instruction, lookIdeas, concepts })).isError, ).toBe(undefined); // Each concept is recorded against its run before the run's assignment fires it. - for (const [index, label] of ["Sleek", "Pastel"].entries()) { + for (const [index, label] of ["Sleek", "Pastel", "Retro"].entries()) { const runId = "run-" + String(index + 1); expect(events.indexOf("concept " + runId + " " + label)).toBeLessThan( events.indexOf("mail " + label), ); } - expect(events.filter((event) => event === "deploy")).toHaveLength(2); + expect(events.filter((event) => event === "deploy")).toHaveLength(3); }); function writerTools(preferences: Record = {}) { diff --git a/apps/hub/src/projects/deploy-tools.ts b/apps/hub/src/projects/deploy-tools.ts index aab3e421..9df78c23 100644 --- a/apps/hub/src/projects/deploy-tools.ts +++ b/apps/hub/src/projects/deploy-tools.ts @@ -45,7 +45,7 @@ import { import { agentDecidesApprovals, asksOpenQuestions, - designConceptCount, + designConceptCeiling, preferenceBlock, } from "../config/preferences.ts"; import { @@ -291,7 +291,7 @@ export function createDeployTools(options?: { { name: "design_app", description: - "Have the designer make or change the app's design from the filed problem brief, constraints, approaches and product requirements: a clickable, responsive mockup with a DESIGN.md of its tokens, screens, states and flows. The coder builds only from a filed design. instruction says what to design or change; a designer already working takes it in the same design. lookIdeas carries the person's own ideas for the look in their words, or that they left the look to you, and every designer works from it. The designer starts once a person approves the instruction. Each design arrives later as a message.\n\nWhen the person wants one suggested design, give one concept: the look you suggest, shaped by their ideas. When they want to explore several, give a concept for each look, up to the number of design concepts the person asked for. One designer per concept makes the design in that concept's look and files it as that concept's design_option for the person to compare; a designer already working on a concept takes the instruction in the same option. Choose concepts that differ clearly from one another, from this catalog or of your own that fit the idea better. Each catalog concept starts from a design theme (" + + "Have the designer make or change the app's design from the filed problem brief, constraints, approaches and product requirements: a clickable, responsive mockup with a DESIGN.md of its tokens, screens, states and flows. The coder builds only from a filed design. instruction says what to design or change; a designer already working takes it in the same design. lookIdeas carries the person's own ideas for the look in their words, or that they left the look to you, and every designer works from it. The designer starts once a person approves the instruction. Each design arrives later as a message.\n\nWhen the person wants one suggested design, give one concept: the look you suggest, shaped by their ideas. When they want to explore several, give a concept for each look: as many as the person asked for, or the number their Design concepts preference names when they said no number. What they say in the conversation wins over that preference. At most five concepts can be at work at a time, counting designers already started. One designer per concept makes the design in that concept's look and files it as that concept's design_option for the person to compare; a designer already working on a concept takes the instruction in the same option. Choose concepts that differ clearly from one another, from this catalog or of your own that fit the idea better. Each catalog concept starts from a design theme (" + DESIGN_THEMES.map((theme) => theme.label).join(", ") + ") and its direction says how its designer adjusts that theme's tokens; a concept of your own starts from the kit's neutral theme, so its direction names the colours, type and radii it wants. When the person chose a theme, design that one concept, with the changes they asked for in its direction:\n" + DESIGN_CONCEPTS.map( @@ -730,8 +730,10 @@ export function createDeployTools(options?: { result = { runId: live.runId, role: "coder" }; } else if (call.name === "design_app") { const input = validArguments(DesignApp, call.arguments); - const { inFlight, preferences } = await listed(); - const limit = designConceptCount(preferences); + const { inFlight } = await listed(); + // The person decides how many concepts start; the preference guides the coordinator when + // they named no number, so only the ceiling holds here. + const limit = designConceptCeiling(); // A requested concept already at work takes the instruction rather than adding a designer. const concepts = new Set( inFlight.flatMap((run) => @@ -741,9 +743,9 @@ export function createDeployTools(options?: { for (const concept of input.concepts) concepts.add(concept.label); if (concepts.size > limit) throw new Refusal( - "too_many_concepts: the person asked for at most " + + "too_many_concepts: at most " + String(limit) + - " design concepts at a time", + " design concepts can be at work at a time, counting those already started", ); // Each concept has its own long-lived designer. const design = (concept: DesignConcept) => From e2a6787994a67f30c787b21c5174992af7cf3c10 Mon Sep 17 00:00:00 2001 From: "Brian J. Fox" Date: Sun, 8 Nov 2026 13:46:43 +0000 Subject: [PATCH 2/3] docs(manual): say the number of concepts you ask for wins --- docs/USER_MANUAL.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/USER_MANUAL.md b/docs/USER_MANUAL.md index 77942985..d54230b1 100644 --- a/docs/USER_MANUAL.md +++ b/docs/USER_MANUAL.md @@ -248,8 +248,10 @@ the **Design concepts to compare** it will make. - **Start the design** lets it begin. - **Not yet** stops it so you can say what to change. -If you asked to explore, you get as many concepts as **Design concepts to -compare** says (three by default). +If you asked to explore, you get as many concepts as you asked for, up to five +at a time. If you did not say how many, you get as many as **Design concepts to +compare** says (three by default). What you say in the conversation wins over +that setting. ### Comparing concepts From 0bb0bf9f8f6ffc6d9a176d40510624e61c367394 Mon Sep 17 00:00:00 2001 From: "Brian J. Fox" Date: Sun, 8 Nov 2026 13:47:57 +0000 Subject: [PATCH 3/3] style(hub): format the design-concept guidance --- apps/hub/src/config/preferences.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/hub/src/config/preferences.ts b/apps/hub/src/config/preferences.ts index 10cfbe0f..108226c3 100644 --- a/apps/hub/src/config/preferences.ts +++ b/apps/hub/src/config/preferences.ts @@ -109,8 +109,7 @@ export const PREFERENCES: readonly PreferenceEntry[] = [ { id: "one", label: "One", - guidance: - "Start one design concept, unless the person asks for more.", + guidance: "Start one design concept, unless the person asks for more.", }, { id: "two",