@teamscala/service-boot 0.23.309 → 0.23.310

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "name": "@teamscala/service-boot",
8
8
  "type": "module",
9
- "version": "0.23.309",
9
+ "version": "0.23.310",
10
10
  "sideEffects": false,
11
11
  "files": [
12
12
  "src"
@@ -55,6 +55,7 @@
55
55
  */
56
56
  import * as v from "valibot";
57
57
  import type { Prisma, WorkItem } from "./task-sync-router.ts";
58
+ import { type Context } from "./auth-router.ts";
58
59
  /**
59
60
  * The assignment input. `name` + `description` are REQUIRED TOGETHER at the
60
61
  * signature: oRPC parses this schema before the handler runs, so a call missing
@@ -194,3 +195,46 @@ export interface AssignmentResult {
194
195
  task: WorkItem;
195
196
  emitted: boolean;
196
197
  }
198
+ /**
199
+ * createBriefTask ORPC input. The MINT entry point a spawner calls to turn a
200
+ * raw brief into the task row a lane self-briefs from. `name` is the lane's work
201
+ * label (the spawner's windowName); `description` is the brief, stored as the
202
+ * task's FIRST COMMENT — never on the task row itself, so "the brief" has
203
+ * exactly one home (`task-sync-brief.ts` readLatestTaskComment, newest-first).
204
+ * Mirrors `assignTaskInput` minus the spawn-shaping fields (slug/windowName/
205
+ * idempotencyKey), which belong on the spawn emission, not the mint.
206
+ */
207
+ export declare const createBriefTaskInput: v.ObjectSchema<{
208
+ readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "name is required — the lane's work label">]>;
209
+ readonly description: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "description is required — it is the brief (first comment)">]>;
210
+ readonly parent_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
211
+ readonly organisation_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
212
+ }, undefined>;
213
+ /**
214
+ * createBriefTask ORPC handler. `spawn_agent_tab` calls this when it receives
215
+ * an `initialPrompt` with no `workItemId`: the prompt becomes the task's first
216
+ * comment, the returned `work_item_id` rides the spawn payload as `workItemId`,
217
+ * and the lane PULLS the brief at boot (`readAssignedBrief`) — no keystroke, no
218
+ * channel send, no positional argument. That deletes the keystroke-brief
219
+ * failure class by construction (`buildAssignmentSpawnPayload` already carries
220
+ * `workItemId` and never `initialPrompt`).
221
+ *
222
+ * Wraps the pure `createTaskWithBriefComment` (atomic task + first comment). The
223
+ * task is `kind:"task"`, `status:"active"`, `assignee_type:"agent"`, with NO
224
+ * `kind_data.lane.session`: the CLI mints the session at spawn and SessionStart
225
+ * ADOPTS this task by id (`adoptAssignmentTask`), which binds the brief to the
226
+ * lane. Returns only the `work_item_id` — the single fact the spawner needs to
227
+ * carry the brief without writing it into the spawn payload. Same transport as
228
+ * `readAssignedBrief`: page-gate-exempt (fn.taskSync), localhost super-admin.
229
+ */
230
+ export declare const createBriefTaskHandler: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
231
+ headers: Headers;
232
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
233
+ prisma: any;
234
+ user: import("./auth-router.ts").User;
235
+ orgId: string | null | undefined;
236
+ }>, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
237
+ work_item_id: string;
238
+ }, {
239
+ work_item_id: string;
240
+ }>, Record<never, never>, Record<never, never>>;
@@ -13,6 +13,7 @@ import { describe, it, expect } from "bun:test";
13
13
  import * as v from "valibot";
14
14
  import {
15
15
  assignTaskInput,
16
+ createBriefTaskInput,
16
17
  createTaskWithBriefComment,
17
18
  adoptAssignmentTask,
18
19
  bindTaskToKnownSession,
@@ -90,6 +91,37 @@ describe("assignTaskInput (incomplete task is unconstructible)", () => {
90
91
  });
91
92
  });
92
93
 
94
+ describe("createBriefTaskInput (the spawner's mint entry — incomplete task is unconstructible)", () => {
95
+ // The mint a raw initialPrompt hits: spawn_agent_tab calls createBriefTask,
96
+ // which validates against this schema BEFORE the handler runs. An incomplete
97
+ // mint (no brief) is rejected at the signature, never caught after the fact —
98
+ // the same gate assignTaskInput sets, on the entry point spawn_agent_tab owns.
99
+ it("accepts name + description together", () => {
100
+ const out = v.safeParse(createBriefTaskInput, {
101
+ name: "Spawn mints row",
102
+ description: "the brief text",
103
+ });
104
+ expect(out.success).toBe(true);
105
+ });
106
+
107
+ it("rejects a missing description — no brief, no minted task", () => {
108
+ expect(v.safeParse(createBriefTaskInput, { name: "X" }).success).toBe(false);
109
+ });
110
+
111
+ it("rejects a missing name", () => {
112
+ expect(v.safeParse(createBriefTaskInput, { description: "d" }).success).toBe(false);
113
+ });
114
+
115
+ it("rejects an empty-string description (not just absent)", () => {
116
+ expect(v.safeParse(createBriefTaskInput, { name: "X", description: "" }).success).toBe(false);
117
+ });
118
+
119
+ it("treats parent_id/organisation_id as optional (exploratory lane)", () => {
120
+ const out = v.safeParse(createBriefTaskInput, { name: "Explore", description: "d" });
121
+ expect(out.success).toBe(true);
122
+ });
123
+ });
124
+
93
125
  describe("createTaskWithBriefComment (atomic task + first comment = brief)", () => {
94
126
  it("creates the task active with kind_data carrying NO session, and writes the description as the first comment in the SAME transaction", async () => {
95
127
  const { prisma, calls } = fakePrisma();
@@ -57,6 +57,7 @@
57
57
  import { getPlatformOrgId } from "@teamscala/db/registry/platform";
58
58
  import * as v from "valibot";
59
59
  import type { Prisma, WorkItem } from "./task-sync-router.ts";
60
+ import { authed, type Context } from "./auth-router.ts";
60
61
 
61
62
  /**
62
63
  * The assignment input. `name` + `description` are REQUIRED TOGETHER at the
@@ -288,3 +289,57 @@ export interface AssignmentResult {
288
289
  task: WorkItem;
289
290
  emitted: boolean;
290
291
  }
292
+
293
+ /**
294
+ * createBriefTask ORPC input. The MINT entry point a spawner calls to turn a
295
+ * raw brief into the task row a lane self-briefs from. `name` is the lane's work
296
+ * label (the spawner's windowName); `description` is the brief, stored as the
297
+ * task's FIRST COMMENT — never on the task row itself, so "the brief" has
298
+ * exactly one home (`task-sync-brief.ts` readLatestTaskComment, newest-first).
299
+ * Mirrors `assignTaskInput` minus the spawn-shaping fields (slug/windowName/
300
+ * idempotencyKey), which belong on the spawn emission, not the mint.
301
+ */
302
+ export const createBriefTaskInput = v.object({
303
+ name: v.pipe(v.string(), v.minLength(1, "name is required — the lane's work label")),
304
+ description: v.pipe(
305
+ v.string(),
306
+ v.minLength(1, "description is required — it is the brief (first comment)"),
307
+ ),
308
+ parent_id: v.optional(v.string()),
309
+ organisation_id: v.optional(v.string()),
310
+ });
311
+
312
+ /**
313
+ * createBriefTask ORPC handler. `spawn_agent_tab` calls this when it receives
314
+ * an `initialPrompt` with no `workItemId`: the prompt becomes the task's first
315
+ * comment, the returned `work_item_id` rides the spawn payload as `workItemId`,
316
+ * and the lane PULLS the brief at boot (`readAssignedBrief`) — no keystroke, no
317
+ * channel send, no positional argument. That deletes the keystroke-brief
318
+ * failure class by construction (`buildAssignmentSpawnPayload` already carries
319
+ * `workItemId` and never `initialPrompt`).
320
+ *
321
+ * Wraps the pure `createTaskWithBriefComment` (atomic task + first comment). The
322
+ * task is `kind:"task"`, `status:"active"`, `assignee_type:"agent"`, with NO
323
+ * `kind_data.lane.session`: the CLI mints the session at spawn and SessionStart
324
+ * ADOPTS this task by id (`adoptAssignmentTask`), which binds the brief to the
325
+ * lane. Returns only the `work_item_id` — the single fact the spawner needs to
326
+ * carry the brief without writing it into the spawn payload. Same transport as
327
+ * `readAssignedBrief`: page-gate-exempt (fn.taskSync), localhost super-admin.
328
+ */
329
+ export const createBriefTaskHandler = authed.handler(async ({ input, context }) => {
330
+ const ctx = context as Context;
331
+ const prisma = ctx.prisma as Prisma;
332
+ const data = input as v.InferOutput<typeof createBriefTaskInput>;
333
+ const userId = (ctx.user as { id: string }).id;
334
+ const task = await createTaskWithBriefComment(
335
+ prisma,
336
+ {
337
+ name: data.name,
338
+ description: data.description,
339
+ parent_id: data.parent_id,
340
+ organisation_id: resolveAssignOrg(data.organisation_id),
341
+ },
342
+ userId,
343
+ );
344
+ return { work_item_id: task.id };
345
+ });
@@ -319,6 +319,18 @@ export declare const taskSyncRouter: {
319
319
  brief: string | null;
320
320
  comment_created_at: string | null;
321
321
  }>, Record<never, never>, Record<never, never>>;
322
+ /** SPAWNER→DB brief mint: `spawn_agent_tab` turns an initialPrompt into the task row (brief = first comment) the lane self-briefs from; returns the work_item_id that rides the spawn payload. Handler in task-sync-assign.ts. */
323
+ createBriefTask: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
324
+ headers: Headers;
325
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
326
+ prisma: any;
327
+ user: import("./auth-router.ts").User;
328
+ orgId: string | null | undefined;
329
+ }>, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
330
+ work_item_id: string;
331
+ }, {
332
+ work_item_id: string;
333
+ }>, Record<never, never>, Record<never, never>>;
322
334
  /**
323
335
  * DB→AGENT realtime leg (explicit dispatch path). Record an out-of-band
324
336
  * change to a subtask's status (operator toggles it in the projects UI,
@@ -42,7 +42,7 @@ import { authed, type Context } from "./auth-router.ts";
42
42
  import { notifyLaneOfTaskChange } from "./task-sync-dispatch.ts";
43
43
  import { resolveLaneTask, resolveLaneWorkName } from "./task-sync-lane.ts";
44
44
  import { readAssignedBriefHandler } from "./task-sync-brief.ts";
45
- import { adoptAssignmentTask } from "./task-sync-assign.ts";
45
+ import { adoptAssignmentTask, createBriefTaskHandler } from "./task-sync-assign.ts";
46
46
  import * as v from "valibot";
47
47
  import {
48
48
  STATUS_TO_WORK_ITEM,
@@ -949,6 +949,9 @@ export const taskSyncRouter = {
949
949
  */
950
950
  readAssignedBrief: readAssignedBriefHandler,
951
951
 
952
+ /** SPAWNER→DB brief mint: `spawn_agent_tab` turns an initialPrompt into the task row (brief = first comment) the lane self-briefs from; returns the work_item_id that rides the spawn payload. Handler in task-sync-assign.ts. */
953
+ createBriefTask: createBriefTaskHandler,
954
+
952
955
  /**
953
956
  * DB→AGENT realtime leg (explicit dispatch path). Record an out-of-band
954
957
  * change to a subtask's status (operator toggles it in the projects UI,