feat(service): take a brief on tasks.start and hand it to Luke
Summary
PR 4 of the New task, prompt-first redesign. A task's start (tasks.start / POST /api/plans) takes an optional brief: the developer's own words about the task. Luke gets it the same way he gets a Linear issue the task was started from. This PR adds the field end to end. PR 5 makes the desktop send it.
- Wire:
taskCreateRequestSchematakesbrief: optionalKey(Trim + maxLength(TASK_BOUNDS.MAX_BRIEF_CHARS)), withMAX_BRIEF_CHARS = 16_000. That matches a coding agent's prompt bound (CODING_AGENT_BOUNDS.MAX_MESSAGE_CHARS). A brief that is only whitespace gets the same treatment as a blank name: it is admitted and stored as none. - Body cap: the old 8 KiB
MAXIMUM_NAME_BODY_BYTEScan't hold a brief at its bound. The collection POST's cap,MAXIMUM_TASK_START_BODY_BYTES(from #3698's atomic start), now also countsMAXIMUM_BRIEF_BYTES = MAX_BRIEF_CHARS * 6, which covers the bound with every character escaped. PATCH keeps the 8 KiB cap. - Host:
TASKS_STARTand the gateway method reusetaskCreateRequestSchema, so they admit the field already.host-operator.tsnow forwardsbriefthe way it forwardslinearIssueId. - Database: new nullable
task.brieftext column. Migration20261018195221_task_add_briefis a single idempotentADD COLUMN IF NOT EXISTS. That is an expand-only step: older code neither inserts nor reads the column.createTaskstores the trimmed brief (null when blank), andStoredTask.briefreads it back. - Brain:
brainStandingContextadds[the developer's brief for the task]and the brief as a JSON string after the task's name and before the Linear issue. It is added on every turn, as the Linear issue is, so Luke has it from the first turn on.BRAIN_EXPLORE_ASK(the opening message of a call, where the Linear issue is explained to the model) now says the brief is the developer's own description of the task to plan from. Its opening line now says the developer "has not spoken yet" instead of "has not said what they want yet", since with a brief they have.
planning: false (or an agent start, which implies it) with a brief: stored and ignored. It is not refused. A start without planning opens no call, so nothing reads the brief. If the developer later opens a call on that task, Luke reads it like any other brief. Nothing in the code argued for refusing it, and refusing would make PR 5's desktop strip the field depending on mode.
Credentials rule: the brief goes only to the task row and the brain's standing context, which is the conversation and the model. It never goes into a counted event, a trace, or a fixture. The test briefs are made-up words.
Evidence
pnpm exec vitest run apps/web/tests/tasks-app.test.ts packages/database/src/models/__tests__/task-store.test.ts packages/service/src/agents/brain-host/brain-host-standing-context.test.ts packages/host/src/host-operator.test.ts: 52 passedpnpm exec vitest run --project features --project host: 947 passedtscfor@luke/features,@luke/database,@luke/host,@luke/service,@luke/web: cleanpnpm --filter @luke/database check:migrations: fine. Lint on the changed files: clean.pnpm check:affecteddid not finish in this sandbox: typecheck, vitest workers anddiscarded-effectran out of memory on an 8 GB, 4-core box. I ran its parts one by one above, and CI runs the whole thing.
Tests added:
- Endpoint: a start keeps its brief. A brief at its bound made of control characters (six bytes each, escaped) is admitted, which fails without the raised body cap. A brief one character past the bound is refused
INVALID_REQUEST. - Store (
task-store.test.ts,getTestDB()): a brief reads back trimmed. None and blank both read none. Aplanning: falsestart keeps its brief. - Brain (
brain-host-standing-context.test.ts, alongside the Linear issue test): a briefed task's standing context carries the marker and the brief, and a plain task's has neither. - Host operator: a start with a brief carries it to the gateway. Starts without one carry no
briefkey, as an old desktop's request does.
Invariants
-
./scripts/check.shgreen. (CI) - JSON Schema goldens:
task-wire-taskCreateRequestSchema.jsongains exactly thebriefproperty (type: string,maxLength: 16000), regenerated withLUKE_UPDATE_FIXTURES=1. No other golden changed. - Every behavior change has its test in this PR. No change-detector test.
- Gateway envelope goldens unchanged. (CI)
- No new
astype assertion. (CI) - No
Effect.run*outside the edges. (CI) - No new raw async primitives. (CI)
- No discarded Effect. (CI)
- Test count: +3 tests (endpoint brief, store brief, brain brief). One existing host-operator test now covers one more start.
- No hand-rolled file replaced.
- No
CLAUDE.md/AGENTS.mdsentence names a changed part. -
PRIVACY.mdunchanged: the brief is the developer's words, kept like the plan's text. - No dependency changes.
- No Node-reaching module behind a renderer barrel.
Notes
- Rebased onto
mainafter #3698 (atomic task-and-agent start) merged. The request schema now carries bothagentandbrief, the golden gains onlybrief, and the cap adds the brief's bytes to #3698's. docs/WORKFLOW.md'srun.sh --previewstep has not been run, because no desktop sends a brief yet. An old desktop's start (no brief) is covered by the tests above.
Made with Luke