@teamscala/service-boot 0.23.322 → 0.23.323
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.
|
|
9
|
+
"version": "0.23.323",
|
|
10
10
|
"sideEffects": false,
|
|
11
11
|
"files": [
|
|
12
12
|
"src"
|
|
@@ -426,6 +426,14 @@
|
|
|
426
426
|
"types": "./src/fn-routers/task-sync-projection.d.ts",
|
|
427
427
|
"default": "./src/fn-routers/task-sync-projection.ts"
|
|
428
428
|
},
|
|
429
|
+
"./fn-routers/task-sync-rename": {
|
|
430
|
+
"types": "./src/fn-routers/task-sync-rename.d.ts",
|
|
431
|
+
"default": "./src/fn-routers/task-sync-rename.ts"
|
|
432
|
+
},
|
|
433
|
+
"./fn-routers/task-sync-rename.ts": {
|
|
434
|
+
"types": "./src/fn-routers/task-sync-rename.d.ts",
|
|
435
|
+
"default": "./src/fn-routers/task-sync-rename.ts"
|
|
436
|
+
},
|
|
429
437
|
"./fn-routers/task-sync-router": {
|
|
430
438
|
"types": "./src/fn-routers/task-sync-router.d.ts",
|
|
431
439
|
"default": "./src/fn-routers/task-sync-router.ts"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Lane RENAME back-write for task-sync: a tab rename EMITS to update the bound
|
|
7
|
+
* work_items task's title in the DB, by CLI session id. This is the DB half of
|
|
8
|
+
* `tab-rename-emits-back-to-task-title`: the operator's tab bar is how they see
|
|
9
|
+
* what is being worked on (`lane-labels-and-task-lists`), so the tmux window
|
|
10
|
+
* name and the task title must never diverge silently. Renaming a tab from
|
|
11
|
+
* `scala-tools session rename` (or the lane renaming itself through the same
|
|
12
|
+
* surface) updates the task title so the project board agrees with the tab bar.
|
|
13
|
+
*
|
|
14
|
+
* THE EMISSION, not a poll (`actions-are-rows-then-emissions`): a caller renames
|
|
15
|
+
* the window (a row/state change) and that change emits the title update — the
|
|
16
|
+
* task-sync owner reacts by updating the row. State lives in the row, never in a
|
|
17
|
+
* return value a caller may never receive.
|
|
18
|
+
*
|
|
19
|
+
* Bound by CLI SESSION ID, never pane id (`lane-identity-is-cli-session-id`): a
|
|
20
|
+
* pane OUTLIVES the session inside it, so matching on a pane would let a dead
|
|
21
|
+
* session's task answer for a live lane. The session is the stable identity a
|
|
22
|
+
* rename resolves to (the daemon resolves the pane's tab → its stored session id
|
|
23
|
+
* and passes that here), so the rename lands on the lane that owns the tab.
|
|
24
|
+
*
|
|
25
|
+
* Split out of task-sync-router.ts for the same reason task-sync-brief/lane/
|
|
26
|
+
* assign exist: the router sits at its 1000-line commit gate, so every cohesive
|
|
27
|
+
* task-sync unit that can live in a sibling does. This owns the rename handler +
|
|
28
|
+
* its pure outcome decision.
|
|
29
|
+
*
|
|
30
|
+
* CAPABILITY HARVEST (capability-harvest-on-split): APP-LOCAL, not a capability
|
|
31
|
+
* candidate. Bound to the work_items schema and to task-sync's own
|
|
32
|
+
* kind_data.lane.session convention; no unrelated app could install this
|
|
33
|
+
* unmodified.
|
|
34
|
+
*/
|
|
35
|
+
import * as v from "valibot";
|
|
36
|
+
import { type Context } from "./auth-router.ts";
|
|
37
|
+
/**
|
|
38
|
+
* renameLaneTask ORPC input. `lane_session` is the lane's CLI-native session id
|
|
39
|
+
* (kind_data.lane.session) — the stable identity a pane rename resolves to.
|
|
40
|
+
* `title` is the new task title (the renamed tab's label). Both required:
|
|
41
|
+
* oRPC rejects a call missing either at the signature
|
|
42
|
+
* (`prevention-over-detection`), so a malformed rename is a 400, never a silent
|
|
43
|
+
* no-op that leaves the title stale.
|
|
44
|
+
*/
|
|
45
|
+
export declare const renameLaneTaskInput: v.ObjectSchema<{
|
|
46
|
+
readonly lane_session: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "lane_session is required">]>;
|
|
47
|
+
readonly title: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "title is required">]>;
|
|
48
|
+
}, undefined>;
|
|
49
|
+
/**
|
|
50
|
+
* The rename outcome from a resolved lane task — pure so the "no task bound ⇒
|
|
51
|
+
* not updated, never an error" decision is unit-testable with no DB. A rename
|
|
52
|
+
* whose lane has no task is best-effort (returns updated:false): every spawn
|
|
53
|
+
* now mints a task, so this is the rare case (a tab whose task was archived and
|
|
54
|
+
* pruned), not a path the operator hits. Failing loudly here would refuse a
|
|
55
|
+
* rename of the WINDOW for a DB miss, which couples two surfaces that must not
|
|
56
|
+
* be — the tmux rename already succeeded before this is called.
|
|
57
|
+
*/
|
|
58
|
+
export declare function renameOutcome(task: {
|
|
59
|
+
id: string;
|
|
60
|
+
} | null): {
|
|
61
|
+
task_id: string | null;
|
|
62
|
+
updated: boolean;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* renameLaneTask ORPC handler. The session-picker daemon calls this from
|
|
66
|
+
* `/rpc/rename` after it renames the tmux window: it resolves the pane's tab to
|
|
67
|
+
* its session id and posts it here. Same transport as bindLane/readTaskList:
|
|
68
|
+
* page-gate-exempt (fn.taskSync), localhost super-admin. Resolves the lane's
|
|
69
|
+
* task BY SESSION (never pane) and updates its title. Idempotent: renaming to
|
|
70
|
+
* the same title writes the same row.
|
|
71
|
+
*
|
|
72
|
+
* An archived parent is matched too (resolveLaneTask falls back to the archived
|
|
73
|
+
* lane task for the session), so a rename of a tab whose run closed still lands
|
|
74
|
+
* on its task — the title is the title regardless of run state.
|
|
75
|
+
*/
|
|
76
|
+
export declare const renameLaneTaskHandler: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
|
|
77
|
+
headers: Headers;
|
|
78
|
+
resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
|
|
79
|
+
prisma: any;
|
|
80
|
+
user: import("./auth-router.ts").User;
|
|
81
|
+
orgId: string | null | undefined;
|
|
82
|
+
}>, v.ObjectSchema<{
|
|
83
|
+
readonly lane_session: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "lane_session is required">]>;
|
|
84
|
+
readonly title: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "title is required">]>;
|
|
85
|
+
}, undefined>, import("@orpc/contract").Schema<{
|
|
86
|
+
task_id: string | null;
|
|
87
|
+
updated: boolean;
|
|
88
|
+
}, {
|
|
89
|
+
task_id: string | null;
|
|
90
|
+
updated: boolean;
|
|
91
|
+
}>, Record<never, never>, Record<never, never>>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit directly
|
|
5
|
+
*
|
|
6
|
+
* Unit tests for the rename back-write leg: renameOutcome (the pure
|
|
7
|
+
* no-task ⇒ not-updated decision) + source-contract assertions that the handler
|
|
8
|
+
* resolves the lane task BY SESSION (never pane) and updates its title. Covers
|
|
9
|
+
* `what-must-have-a-test` for the new procedure + the `lane-identity-is-cli-
|
|
10
|
+
* session-id` invariant it rides on.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect } from "bun:test";
|
|
13
|
+
import { renameOutcome } from "./task-sync-rename.ts";
|
|
14
|
+
|
|
15
|
+
describe("renameOutcome (no verdict from no data)", () => {
|
|
16
|
+
it("returns not-updated with a null task_id when no task is bound to the session", () => {
|
|
17
|
+
// A rename of a tab whose lane has no resolvable task updates nothing —
|
|
18
|
+
// never an error, never a fake id. updated:false is the signal the caller
|
|
19
|
+
// treats as best-effort (the window was already renamed; this only missed
|
|
20
|
+
// the DB echo, which is the rare archived-and-pruned case).
|
|
21
|
+
expect(renameOutcome(null)).toEqual({ task_id: null, updated: false });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns the bound task id + updated:true when a task resolves", () => {
|
|
25
|
+
expect(renameOutcome({ id: "task-9" })).toEqual({ task_id: "task-9", updated: true });
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("renameLaneTask resolves BY SESSION and writes the title (source contract)", () => {
|
|
30
|
+
const renameSrc = Bun.file(new URL("./task-sync-rename.ts", import.meta.url).pathname);
|
|
31
|
+
|
|
32
|
+
it("resolves the lane task by lane_session (the CLI session id), never by pane", async () => {
|
|
33
|
+
// `lane-identity-is-cli-session-id`: binding is by session because a pane
|
|
34
|
+
// outlives the session inside it. A pane-keyed rename would let a dead
|
|
35
|
+
// session's task answer for a live lane.
|
|
36
|
+
const text = await renameSrc.text();
|
|
37
|
+
expect(text).toContain("resolveLaneTask(prisma, lane_session)");
|
|
38
|
+
// No pane matching anywhere in the resolution path.
|
|
39
|
+
expect(text).not.toContain("lane.pane");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("updates the resolved task's title by id", async () => {
|
|
43
|
+
const text = await renameSrc.text();
|
|
44
|
+
expect(text).toContain('where: { id: task.id }');
|
|
45
|
+
expect(text).toContain("data: { title }");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("requires lane_session + title at the signature (incomplete rename unconstructible)", async () => {
|
|
49
|
+
const text = await renameSrc.text();
|
|
50
|
+
expect(text).toContain('lane_session: v.pipe(v.string()');
|
|
51
|
+
expect(text).toContain('title: v.pipe(v.string()');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Lane RENAME back-write for task-sync: a tab rename EMITS to update the bound
|
|
7
|
+
* work_items task's title in the DB, by CLI session id. This is the DB half of
|
|
8
|
+
* `tab-rename-emits-back-to-task-title`: the operator's tab bar is how they see
|
|
9
|
+
* what is being worked on (`lane-labels-and-task-lists`), so the tmux window
|
|
10
|
+
* name and the task title must never diverge silently. Renaming a tab from
|
|
11
|
+
* `scala-tools session rename` (or the lane renaming itself through the same
|
|
12
|
+
* surface) updates the task title so the project board agrees with the tab bar.
|
|
13
|
+
*
|
|
14
|
+
* THE EMISSION, not a poll (`actions-are-rows-then-emissions`): a caller renames
|
|
15
|
+
* the window (a row/state change) and that change emits the title update — the
|
|
16
|
+
* task-sync owner reacts by updating the row. State lives in the row, never in a
|
|
17
|
+
* return value a caller may never receive.
|
|
18
|
+
*
|
|
19
|
+
* Bound by CLI SESSION ID, never pane id (`lane-identity-is-cli-session-id`): a
|
|
20
|
+
* pane OUTLIVES the session inside it, so matching on a pane would let a dead
|
|
21
|
+
* session's task answer for a live lane. The session is the stable identity a
|
|
22
|
+
* rename resolves to (the daemon resolves the pane's tab → its stored session id
|
|
23
|
+
* and passes that here), so the rename lands on the lane that owns the tab.
|
|
24
|
+
*
|
|
25
|
+
* Split out of task-sync-router.ts for the same reason task-sync-brief/lane/
|
|
26
|
+
* assign exist: the router sits at its 1000-line commit gate, so every cohesive
|
|
27
|
+
* task-sync unit that can live in a sibling does. This owns the rename handler +
|
|
28
|
+
* its pure outcome decision.
|
|
29
|
+
*
|
|
30
|
+
* CAPABILITY HARVEST (capability-harvest-on-split): APP-LOCAL, not a capability
|
|
31
|
+
* candidate. Bound to the work_items schema and to task-sync's own
|
|
32
|
+
* kind_data.lane.session convention; no unrelated app could install this
|
|
33
|
+
* unmodified.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import * as v from "valibot";
|
|
37
|
+
import type { Prisma } from "./task-sync-router.ts";
|
|
38
|
+
import { authed, type Context } from "./auth-router.ts";
|
|
39
|
+
import { resolveLaneTask } from "./task-sync-lane.ts";
|
|
40
|
+
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* renameLaneTask ORPC input. `lane_session` is the lane's CLI-native session id
|
|
44
|
+
* (kind_data.lane.session) — the stable identity a pane rename resolves to.
|
|
45
|
+
* `title` is the new task title (the renamed tab's label). Both required:
|
|
46
|
+
* oRPC rejects a call missing either at the signature
|
|
47
|
+
* (`prevention-over-detection`), so a malformed rename is a 400, never a silent
|
|
48
|
+
* no-op that leaves the title stale.
|
|
49
|
+
*/
|
|
50
|
+
export const renameLaneTaskInput = v.object({
|
|
51
|
+
lane_session: v.pipe(v.string(), v.minLength(1, "lane_session is required")),
|
|
52
|
+
title: v.pipe(v.string(), v.minLength(1, "title is required")),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The rename outcome from a resolved lane task — pure so the "no task bound ⇒
|
|
57
|
+
* not updated, never an error" decision is unit-testable with no DB. A rename
|
|
58
|
+
* whose lane has no task is best-effort (returns updated:false): every spawn
|
|
59
|
+
* now mints a task, so this is the rare case (a tab whose task was archived and
|
|
60
|
+
* pruned), not a path the operator hits. Failing loudly here would refuse a
|
|
61
|
+
* rename of the WINDOW for a DB miss, which couples two surfaces that must not
|
|
62
|
+
* be — the tmux rename already succeeded before this is called.
|
|
63
|
+
*/
|
|
64
|
+
export function renameOutcome(task: { id: string } | null): {
|
|
65
|
+
task_id: string | null;
|
|
66
|
+
updated: boolean;
|
|
67
|
+
} {
|
|
68
|
+
if (!task) return { task_id: null, updated: false };
|
|
69
|
+
return { task_id: task.id, updated: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* renameLaneTask ORPC handler. The session-picker daemon calls this from
|
|
74
|
+
* `/rpc/rename` after it renames the tmux window: it resolves the pane's tab to
|
|
75
|
+
* its session id and posts it here. Same transport as bindLane/readTaskList:
|
|
76
|
+
* page-gate-exempt (fn.taskSync), localhost super-admin. Resolves the lane's
|
|
77
|
+
* task BY SESSION (never pane) and updates its title. Idempotent: renaming to
|
|
78
|
+
* the same title writes the same row.
|
|
79
|
+
*
|
|
80
|
+
* An archived parent is matched too (resolveLaneTask falls back to the archived
|
|
81
|
+
* lane task for the session), so a rename of a tab whose run closed still lands
|
|
82
|
+
* on its task — the title is the title regardless of run state.
|
|
83
|
+
*/
|
|
84
|
+
export const renameLaneTaskHandler = authed
|
|
85
|
+
.input(renameLaneTaskInput)
|
|
86
|
+
.handler(async ({ input, context }) => {
|
|
87
|
+
const ctx = context as Context;
|
|
88
|
+
const prisma = ctx.prisma as Prisma;
|
|
89
|
+
const { lane_session, title } = input;
|
|
90
|
+
const task = await resolveLaneTask(prisma, lane_session);
|
|
91
|
+
const outcome = renameOutcome(task);
|
|
92
|
+
if (!task) return outcome;
|
|
93
|
+
try {
|
|
94
|
+
await prisma.work_items.update({
|
|
95
|
+
where: { id: task.id },
|
|
96
|
+
data: { title },
|
|
97
|
+
select: { id: true },
|
|
98
|
+
});
|
|
99
|
+
} catch (err) {
|
|
100
|
+
// The window was already renamed by the caller; a DB update failure must
|
|
101
|
+
// not surface as an error that implies the rename failed. Report
|
|
102
|
+
// not-updated + log, so the two surfaces stay decoupled
|
|
103
|
+
// (`a-component-may-not-report-a-state-it-has-not-verified`).
|
|
104
|
+
getAppLogger().warn("[task-sync] renameLaneTask: title update failed", {
|
|
105
|
+
taskId: task.id,
|
|
106
|
+
error: err instanceof Error ? err.message : String(err),
|
|
107
|
+
});
|
|
108
|
+
return { task_id: task.id, updated: false };
|
|
109
|
+
}
|
|
110
|
+
return outcome;
|
|
111
|
+
});
|
|
@@ -344,6 +344,28 @@ export declare const taskSyncRouter: {
|
|
|
344
344
|
}, {
|
|
345
345
|
work_item_id: string;
|
|
346
346
|
}>, Record<never, never>, Record<never, never>>;
|
|
347
|
+
/**
|
|
348
|
+
* TAB→DB rename back-write: the session-picker `/rpc/rename` calls this after
|
|
349
|
+
* renaming a tmux window to update the bound task's title BY SESSION (never
|
|
350
|
+
* pane), so the tab bar and the project board never diverge
|
|
351
|
+
* (`tab-rename-emits-back-to-task-title`). Handler in task-sync-rename.ts.
|
|
352
|
+
*/
|
|
353
|
+
renameLaneTask: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
|
|
354
|
+
headers: Headers;
|
|
355
|
+
resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
|
|
356
|
+
prisma: any;
|
|
357
|
+
user: import("./auth-router.ts").User;
|
|
358
|
+
orgId: string | null | undefined;
|
|
359
|
+
}>, v.ObjectSchema<{
|
|
360
|
+
readonly lane_session: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "lane_session is required">]>;
|
|
361
|
+
readonly title: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "title is required">]>;
|
|
362
|
+
}, undefined>, import("@orpc/contract").Schema<{
|
|
363
|
+
task_id: string | null;
|
|
364
|
+
updated: boolean;
|
|
365
|
+
}, {
|
|
366
|
+
task_id: string | null;
|
|
367
|
+
updated: boolean;
|
|
368
|
+
}>, Record<never, never>, Record<never, never>>;
|
|
347
369
|
/**
|
|
348
370
|
* DB→AGENT realtime leg (explicit dispatch path). Record an out-of-band
|
|
349
371
|
* change to a subtask's status (operator toggles it in the projects UI,
|
|
@@ -43,6 +43,7 @@ import { authed, type Context } from "./auth-router.ts";
|
|
|
43
43
|
import { notifyLaneOfTaskChange } from "./task-sync-dispatch.ts";
|
|
44
44
|
import { resolveLaneTask, resolveLaneWorkName } from "./task-sync-lane.ts";
|
|
45
45
|
import { readAssignedBriefHandler } from "./task-sync-brief.ts";
|
|
46
|
+
import { renameLaneTaskHandler } from "./task-sync-rename.ts";
|
|
46
47
|
import {
|
|
47
48
|
adoptAssignmentTask,
|
|
48
49
|
bindLaneInput,
|
|
@@ -772,6 +773,14 @@ export const taskSyncRouter = {
|
|
|
772
773
|
/** 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. */
|
|
773
774
|
createBriefTask: createBriefTaskHandler,
|
|
774
775
|
|
|
776
|
+
/**
|
|
777
|
+
* TAB→DB rename back-write: the session-picker `/rpc/rename` calls this after
|
|
778
|
+
* renaming a tmux window to update the bound task's title BY SESSION (never
|
|
779
|
+
* pane), so the tab bar and the project board never diverge
|
|
780
|
+
* (`tab-rename-emits-back-to-task-title`). Handler in task-sync-rename.ts.
|
|
781
|
+
*/
|
|
782
|
+
renameLaneTask: renameLaneTaskHandler,
|
|
783
|
+
|
|
775
784
|
/**
|
|
776
785
|
* DB→AGENT realtime leg (explicit dispatch path). Record an out-of-band
|
|
777
786
|
* change to a subtask's status (operator toggles it in the projects UI,
|