@teamscala/service-boot 0.23.323 → 0.23.324
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 +524 -532
- package/src/agreement-gate.ts +12 -0
- package/src/auth-gate.test.ts +70 -0
- package/src/auth-gate.ts +68 -6
- package/src/build-client-assets.test.ts +1 -1
- package/src/build-client-assets.ts +1 -1
- package/src/discover-blocks.ts +1 -1
- package/src/fn-routers/index.ts +2 -0
- package/src/fn-routers/task-sync-assign.test.ts +2 -1
- package/src/fn-routers/task-sync-assign.ts +4 -4
- package/src/fn-routers/task-sync-bind.test.ts +18 -18
- package/src/fn-routers/task-sync-brief.test.ts +20 -17
- package/src/fn-routers/task-sync-brief.ts +15 -14
- package/src/fn-routers/task-sync-close.test.ts +44 -0
- package/src/fn-routers/task-sync-close.ts +82 -0
- package/src/fn-routers/task-sync-dispatch.test.ts +51 -33
- package/src/fn-routers/task-sync-lane.test.ts +36 -0
- package/src/fn-routers/task-sync-projection.test.ts +26 -0
- package/src/fn-routers/task-sync-projection.ts +51 -0
- package/src/fn-routers/task-sync-router.test.ts +14 -2
- package/src/handlers/singleton-tools-currency-watch.ts +114 -11
- package/src/handlers/sweep-host-cache.test.ts +133 -0
- package/src/handlers/sweep-host-cache.ts +143 -26
- package/src/install-logging.ts +3 -0
- package/src/mount-orpc-router.ts +1 -1
- package/src/register-page-routes.ts +0 -3
- package/src/resolve-brand-favicon.ts +1 -1
- package/src/resource-not-found.ts +10 -0
- package/src/serve/stamp-transport-identity.test.ts +24 -0
- package/src/serve/stamp-transport-identity.ts +14 -2
- package/src/two-phase-boot.ts +85 -17
- package/src/two-phase-health-gate-client-only.test.ts +77 -0
- package/src/two-phase-health-gate.test.ts +13 -2
- package/src/agreement-gate.d.ts +0 -19
- package/src/asset-cache-key.d.ts +0 -11
- package/src/asset-optimize.d.ts +0 -17
- package/src/asset-proxy.d.ts +0 -11
- package/src/asset-social-card.d.ts +0 -17
- package/src/attach-agreement-magic.d.ts +0 -20
- package/src/attach-portal-members.d.ts +0 -23
- package/src/attach-proposal-accept.d.ts +0 -26
- package/src/attach-proposal-magic.d.ts +0 -18
- package/src/attach-transfers-upload.d.ts +0 -27
- package/src/auth-gate.d.ts +0 -28
- package/src/bind-page-handler.d.ts +0 -18
- package/src/build-client-assets/input-hashing.d.ts +0 -25
- package/src/build-client-assets/source-candidates.d.ts +0 -27
- package/src/build-client-assets.d.ts +0 -50
- package/src/compose-page-title.d.ts +0 -41
- package/src/content-gate/access.d.ts +0 -15
- package/src/content-gate/config-types.d.ts +0 -99
- package/src/content-gate/gate.d.ts +0 -13
- package/src/content-gate/parse-cookie.d.ts +0 -9
- package/src/content-gate/token.d.ts +0 -48
- package/src/content-page-floor.d.ts +0 -39
- package/src/discover-blocks.d.ts +0 -28
- package/src/expand-roster.d.ts +0 -34
- package/src/fn-routers/agreement-completion-email.d.ts +0 -14
- package/src/fn-routers/agreement-send-router.d.ts +0 -82
- package/src/fn-routers/ai-health-router.d.ts +0 -35
- package/src/fn-routers/auth-router.d.ts +0 -47
- package/src/fn-routers/cache-admin-router.d.ts +0 -57
- package/src/fn-routers/chat-session-router.d.ts +0 -219
- package/src/fn-routers/email-defaults.d.ts +0 -1
- package/src/fn-routers/email-link-ttl.d.ts +0 -1
- package/src/fn-routers/index.d.ts +0 -18
- package/src/fn-routers/panel-data-router.d.ts +0 -19
- package/src/fn-routers/persist-chat-messages.d.ts +0 -36
- package/src/fn-routers/plans-router.d.ts +0 -25
- package/src/fn-routers/proposal-send-router.d.ts +0 -58
- package/src/fn-routers/public-agreement-router.d.ts +0 -289
- package/src/fn-routers/public-assessment-router.d.ts +0 -68
- package/src/fn-routers/public-gpt-session-router.d.ts +0 -143
- package/src/fn-routers/sign-in-flow-router.d.ts +0 -107
- package/src/fn-routers/task-sync-assign.d.ts +0 -305
- package/src/fn-routers/task-sync-brief.d.ts +0 -96
- package/src/fn-routers/task-sync-dispatch.d.ts +0 -61
- package/src/fn-routers/task-sync-dispatch.ts +0 -124
- package/src/fn-routers/task-sync-lane.d.ts +0 -49
- package/src/fn-routers/task-sync-lane.ts +0 -125
- package/src/fn-routers/task-sync-projection.d.ts +0 -121
- package/src/fn-routers/task-sync-rename.d.ts +0 -91
- package/src/fn-routers/task-sync-router.d.ts +0 -424
- package/src/fn-routers/task-sync-router.ts +0 -1106
- package/src/fn-routers/tasks-router.d.ts +0 -25
- package/src/fn-routers/user-preferences-router.d.ts +0 -77
- package/src/fn-routers/work-items-router.d.ts +0 -102
- package/src/gate-ttls.d.ts +0 -6
- package/src/gpt-gate.d.ts +0 -25
- package/src/handlers/bun-stable-watch.d.ts +0 -1
- package/src/handlers/pane-outbox-drain.d.ts +0 -1
- package/src/handlers/projects-digest.d.ts +0 -26
- package/src/handlers/prune-stale-data.d.ts +0 -30
- package/src/handlers/registry-stale-inactive-sweep.d.ts +0 -1
- package/src/handlers/scala-tools-currency-watch.d.ts +0 -1
- package/src/handlers/singleton-tools-currency-watch.d.ts +0 -1
- package/src/handlers/sweep-event-log.d.ts +0 -1
- package/src/handlers/sweep-host-cache.d.ts +0 -1
- package/src/handlers/upstream-issue-sync.d.ts +0 -1
- package/src/handlers/whatsapp-liveness-logic.d.ts +0 -34
- package/src/handlers/whatsapp-liveness.d.ts +0 -4
- package/src/hono-error-handler.d.ts +0 -25
- package/src/install-harness.d.ts +0 -45
- package/src/install-logging.d.ts +0 -34
- package/src/load-service-install-row.d.ts +0 -24
- package/src/load-service-prisma-client.d.ts +0 -18
- package/src/mount-orpc-router.d.ts +0 -15
- package/src/mount-server-packages.d.ts +0 -29
- package/src/mount.d.ts +0 -9
- package/src/mutable-app-ref.d.ts +0 -44
- package/src/page-route-shell-types.d.ts +0 -12
- package/src/page-server-orpc/audit-revert-router.d.ts +0 -37
- package/src/page-server-orpc/hub-mcp-client.d.ts +0 -12
- package/src/page-server-orpc/portal-diagnostics-router.d.ts +0 -49
- package/src/page-server-orpc/puck-pages-router.d.ts +0 -58
- package/src/page-server-orpc/server-client.d.ts +0 -16
- package/src/page-server-orpc/templates-router.d.ts +0 -34
- package/src/page-server-orpc/user-preferences-router.d.ts +0 -79
- package/src/pg-boss-handler-registry.d.ts +0 -27
- package/src/pg-boss-mount.d.ts +0 -42
- package/src/proposal-authorized-emails.d.ts +0 -28
- package/src/proposal-chat-inject.d.ts +0 -17
- package/src/proposal-gate.d.ts +0 -18
- package/src/read-html-template.d.ts +0 -38
- package/src/read-viewport-from-request.d.ts +0 -18
- package/src/readiness-gate.d.ts +0 -16
- package/src/register-api-routes.d.ts +0 -18
- package/src/register-crash-handlers.d.ts +0 -15
- package/src/register-page-routes.d.ts +0 -27
- package/src/register-page-server-client.d.ts +0 -11
- package/src/register-registry-page-route/ai-chat-prefetch.d.ts +0 -13
- package/src/register-registry-page-route/graph-cache.d.ts +0 -30
- package/src/register-registry-page-route/meta-title.d.ts +0 -2
- package/src/register-registry-page-route/proposal-approval-email-token.d.ts +0 -14
- package/src/register-registry-page-route/proposal-brand-stamping.d.ts +0 -28
- package/src/register-registry-page-route/resolve-page-data.d.ts +0 -1
- package/src/register-registry-page-route.d.ts +0 -27
- package/src/register-static-assets.d.ts +0 -13
- package/src/register-ws-routes.d.ts +0 -28
- package/src/resolve-app-base-url.d.ts +0 -13
- package/src/resolve-brand-favicon.d.ts +0 -28
- package/src/resolve-from-loader-config.d.ts +0 -32
- package/src/resolve-service-context.d.ts +0 -17
- package/src/resolve-service-port.d.ts +0 -75
- package/src/resolve-theme-mode.d.ts +0 -42
- package/src/resolvers/fetch-org-brand.d.ts +0 -2
- package/src/resolvers/portal-route-paths.d.ts +0 -39
- package/src/resolvers/website-by-host-map.d.ts +0 -12
- package/src/resource-chat-inject.d.ts +0 -46
- package/src/resource-not-found.d.ts +0 -60
- package/src/run-boot-hook-list.d.ts +0 -19
- package/src/run-boot-hooks.d.ts +0 -31
- package/src/scala-tools-currency-watchdog.d.ts +0 -10
- package/src/security-headers.d.ts +0 -12
- package/src/serve/serve.d.ts +0 -46
- package/src/serve/stamp-transport-identity.d.ts +0 -32
- package/src/server-error-page.d.ts +0 -29
- package/src/start-scheduled-jobs.d.ts +0 -18
- package/src/two-phase-boot.d.ts +0 -136
- package/src/types.d.ts +0 -46
- package/src/ui-service.d.ts +0 -46
- package/src/ws-boot-self-test.d.ts +0 -19
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @indivisible-unit
|
|
5
|
-
* @edit edit directly
|
|
6
|
-
*
|
|
7
|
-
* Lane DISPATCH by assignment — the WRITE half of dispatch-by-assignment. The
|
|
8
|
-
* operator's contract (verbatim intent): you create a task (the NAME), you write
|
|
9
|
-
* the first COMMENT (which IS the prompt/brief), and THEN you assign it to an AI
|
|
10
|
-
* agent — and the assignment is the TRIGGER that spawns the lane. Assignment
|
|
11
|
-
* last, always, because by the time the agent exists every piece of information
|
|
12
|
-
* it needs is already in the row.
|
|
13
|
-
*
|
|
14
|
-
* This file owns the two pure halves of that flow, split out of task-sync-router
|
|
15
|
-
* (which sits at its 1000-line commit gate, so any new task-sync unit lands in a
|
|
16
|
-
* sibling — the same reason task-sync-brief/lane/dispatch exist):
|
|
17
|
-
* createTaskWithBriefComment — create the task AND its first comment (the
|
|
18
|
-
* description IS the first comment IS the brief) in one transaction. An
|
|
19
|
-
* incomplete task is UNCONSTRUCTIBLE: the ORPC `.input()` schema
|
|
20
|
-
* (`assignTaskInput`) requires name + description TOGETHER, so oRPC rejects
|
|
21
|
-
* the call before the handler runs (`prevention-over-detection` — require at
|
|
22
|
-
* the signature, never validate after the fact).
|
|
23
|
-
* buildAssignmentSpawnPayload — shape the spawn emission that the assignment
|
|
24
|
-
* emits. It carries the task id and NO initialPrompt, because the spawned
|
|
25
|
-
* lane PULLS its brief from the DB by its own session id
|
|
26
|
-
* (`task-sync-brief.ts` readAssignedBrief → latest comment). That deletes the
|
|
27
|
-
* keystroke-brief failure class (a ~4400-char initialPrompt fails outright;
|
|
28
|
-
* a ~2600-char one arrives intact and is never submitted, then the pane is
|
|
29
|
-
* reaped for producing no turn): when the brief is a row, no keystroke can
|
|
30
|
-
* lose it.
|
|
31
|
-
*
|
|
32
|
-
* THE EMISSION is a row write followed by an emission that triggers the spawn
|
|
33
|
-
* (`actions-are-rows-then-emissions`): the assignment writes the task + comment,
|
|
34
|
-
* then emits; the host-command bus consumer reacts by spawning. State lives in
|
|
35
|
-
* the row, never in a return value a caller may never receive. The exact bus
|
|
36
|
-
* surface this file emits through is INJECTED (AssignmentEmitter) rather than
|
|
37
|
-
* hard-coded, so it composes with whichever surface the host-command-bus cutover
|
|
38
|
-
* lands — one mechanism, not two (`no-silent-reverts` on the spawn path).
|
|
39
|
-
*
|
|
40
|
-
* BINDING (the gap this flow exposes). A lane resolves its task by its CLI-native
|
|
41
|
-
* session id (`kind_data.lane.session`, minted by the CLI at boot — never by us).
|
|
42
|
-
* Today `ensureLaneTask`/`bindLane` CREATE a fresh parent task at SessionStart.
|
|
43
|
-
* But assignment PRE-CREATES the task (with the brief as its first comment, no
|
|
44
|
-
* session yet), so SessionStart must ADOPT THAT EXISTING TASK BY ID — writing the
|
|
45
|
-
* freshly-minted session onto its `kind_data.lane.session` — instead of creating
|
|
46
|
-
* a new one, or the brief-task is orphaned and the lane self-briefs to nothing.
|
|
47
|
-
* The task is created `status:"active"` with no session, so `resolveLaneTask`
|
|
48
|
-
* (which filters `kind_data.lane.session`) cannot see it until bind writes the
|
|
49
|
-
* session — exactly right: the lane resolves it only after it is bound.
|
|
50
|
-
*
|
|
51
|
-
* CAPABILITY HARVEST (capability-harvest-on-split): APP-LOCAL, not a capability
|
|
52
|
-
* candidate. Bound to the work_items + comments schema and to task-sync's own
|
|
53
|
-
* kind_data.lane.session convention; no unrelated app could install this
|
|
54
|
-
* unmodified.
|
|
55
|
-
*/
|
|
56
|
-
import * as v from "valibot";
|
|
57
|
-
import type { Prisma, WorkItem } from "./task-sync-router.ts";
|
|
58
|
-
import { type Context } from "./auth-router.ts";
|
|
59
|
-
export declare const assignTaskInput: v.ObjectSchema<{
|
|
60
|
-
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "name is required — the lane's real work label">, v.CheckAction<string, "name must carry the lane's real work name, not the generic sentinel 'Lane'">]>;
|
|
61
|
-
readonly description: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "description is required — it is the brief (first comment)">]>;
|
|
62
|
-
readonly parent_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
63
|
-
readonly organisation_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
64
|
-
readonly slug: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
65
|
-
readonly windowName: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
66
|
-
readonly idempotencyKey: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
67
|
-
}, undefined>;
|
|
68
|
-
/** The resolved (post-schema) shape the handlers operate on. */
|
|
69
|
-
export type AssignTaskInput = v.InferOutput<typeof assignTaskInput>;
|
|
70
|
-
/**
|
|
71
|
-
* bindLane ORPC input. Declaring the schema is the documented remedy for the
|
|
72
|
-
* schema-less-body gap (`upsertFromTaskListInput` cites the same measured bug):
|
|
73
|
-
* oRPC does NOT parse a JSON body into `input` for a procedure with no
|
|
74
|
-
* `.input()`, so a SessionStart adapter that POSTs the work item id as a DIRECT
|
|
75
|
-
* body field (`{lane:{...}, parent_id}`) arrives as an EMPTY input — `parent_id`
|
|
76
|
-
* is silently dropped, `adoptAssignmentTask` gets null, no adoption happens, and
|
|
77
|
-
* `readAssignedBrief` returns nulls for a session that has nothing bound to it.
|
|
78
|
-
* That is a DATA gap (nothing bound to the session), never a serving one — which
|
|
79
|
-
* is exactly the misdiagnosis this closes: a prior lane read those nulls as
|
|
80
|
-
* "fn.taskSync is 404" and reached for serving changes, when the row was simply
|
|
81
|
-
* never bound because the body never parsed.
|
|
82
|
-
*
|
|
83
|
-
* ROOT-OPTIONAL (`v.optional(v.object(...))`) so it tolerates a caller that sends
|
|
84
|
-
* a DIRECT body or none at all: oRPC extracts `input` from the `{json:{...}}` RPC
|
|
85
|
-
* envelope, so a raw-object or empty POST yields `undefined`, which the optional
|
|
86
|
-
* root ACCEPTS — the handler then falls back to its headers, exactly as it did
|
|
87
|
-
* schema-less (a header-only caller like zerostack is unchanged). An adapter that
|
|
88
|
-
* wants the body path (pi's `parent_id`) POSTs the `{json:{...}}` envelope and
|
|
89
|
-
* `parent_id` is parsed + reaches `adoptAssignmentTask` — the same envelope
|
|
90
|
-
* convention pi ALREADY uses for upsertFromTaskList. A raw-object POST is NOT
|
|
91
|
-
* parsed (oRPC hands the handler `undefined`), which is why pi's bindLaneTask
|
|
92
|
-
* must move to the envelope to deliver `parent_id`. Every field is optional with
|
|
93
|
-
* a header fallback, so the schema ADDS the body path without removing the header.
|
|
94
|
-
*/
|
|
95
|
-
export declare const bindLaneInput: v.OptionalSchema<v.ObjectSchema<{
|
|
96
|
-
readonly lane: v.OptionalSchema<v.ObjectSchema<{
|
|
97
|
-
readonly pane: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
98
|
-
readonly session: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
99
|
-
readonly cli_kind: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
100
|
-
readonly role: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
101
|
-
readonly organisation_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
102
|
-
}, undefined>, undefined>;
|
|
103
|
-
readonly title: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
104
|
-
readonly parent_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
105
|
-
}, undefined>, undefined>;
|
|
106
|
-
/**
|
|
107
|
-
* Resolve the org a dispatched task is created under. Pure: the platform org is
|
|
108
|
-
* INJECTED (resolved by the caller at the handler boundary, where a registry
|
|
109
|
-
* read is appropriate) rather than pulled from the registry inside this module,
|
|
110
|
-
* so the create logic is unit-testable with no DB (`configured-primitives`:
|
|
111
|
-
* config arrives from the caller, never pulled from DB/Env inside a primitive).
|
|
112
|
-
*/
|
|
113
|
-
export declare function resolveAssignOrg(explicit?: string, platformOrg?: string): string;
|
|
114
|
-
/**
|
|
115
|
-
* Create a dispatched task AND its first comment (the description = the brief) in
|
|
116
|
-
* one transaction. The task is `kind:"task"`, `status:"active"`, with NO
|
|
117
|
-
* `kind_data.lane.session` yet — the session is minted by the CLI at spawn and
|
|
118
|
-
* written onto the row by SessionStart's adopt-existing bind. Until then the
|
|
119
|
-
* task is invisible to `resolveLaneTask` (which filters on the session), which is
|
|
120
|
-
* correct: the lane may only resolve its task once it is bound to it.
|
|
121
|
-
*
|
|
122
|
-
* Pure over an injected prisma (no module-level I/O) so the atomic create +
|
|
123
|
-
* first-comment decision is unit-testable with a fake transaction. `userId`
|
|
124
|
-
* stamps `created_by`/`updated_by` on the task and `user_id` on the comment.
|
|
125
|
-
*/
|
|
126
|
-
export declare function createTaskWithBriefComment(prisma: Prisma, input: {
|
|
127
|
-
name: string;
|
|
128
|
-
description: string;
|
|
129
|
-
parent_id?: string;
|
|
130
|
-
organisation_id: string;
|
|
131
|
-
}, userId: string): Promise<WorkItem>;
|
|
132
|
-
/**
|
|
133
|
-
* ADOPT a pre-created assignment task at SessionStart: write the lane's
|
|
134
|
-
* freshly-minted session onto an EXISTING task row instead of creating a new
|
|
135
|
-
* one. `bindLane` calls this when the spawn carried a `workItemId` that resolves
|
|
136
|
-
* to a `kind:"task"` row (the assignment task whose first comment IS the brief).
|
|
137
|
-
* Without this, bindLane would CREATE a new task and orphan the brief-task — the
|
|
138
|
-
* lane would `readAssignedBrief` to nothing.
|
|
139
|
-
*
|
|
140
|
-
* Realizes OWNERSHIP too: sets `assignee_id` to the lane's session (its stable
|
|
141
|
-
* identity, `lane-identity-is-cli-session-id`), so the compliance gate can
|
|
142
|
-
* distinguish a task the CALLER owes from one a dispatched lane owns (the
|
|
143
|
-
* orchestrator supervises, not implements — `anthropic-models-orchestrate-not-
|
|
144
|
-
* implement`). "Is the caller the assignee" becomes a simple equality on the
|
|
145
|
-
* session.
|
|
146
|
-
*
|
|
147
|
-
* RE-BINDS a task another session already owns. Migration 109
|
|
148
|
-
* (`work_items_one_active_task_per_lane`) makes `(kind_data->'lane'->>'pane')`
|
|
149
|
-
* unique WHERE active, so when a lane is re-spawned on a pane whose prior task is
|
|
150
|
-
* still active under a STALE session (the lane died, the row survived),
|
|
151
|
-
* `resolveLaneTask(newSession)` finds nothing and `create` P2002s — measured
|
|
152
|
-
* fleet-wide 2026-08-01: every real lane's bindLane 500'd on this constraint.
|
|
153
|
-
* Refusing to steal (the original design) routes every re-spawn into that
|
|
154
|
-
* dead-end. An assignment task belongs to whichever lane was dispatched to it,
|
|
155
|
-
* and a stale session is by definition dead, so re-binding to the fresh session
|
|
156
|
-
* is the correct recovery: the pane's unique slot moves to the live lane and
|
|
157
|
-
* the brief follows. Returns null only when `maybeTaskId` is absent or not a
|
|
158
|
-
* task — caller falls through to create for an exploratory lane (no workItemId).
|
|
159
|
-
*/
|
|
160
|
-
export declare function adoptAssignmentTask(prisma: Prisma, maybeTaskId: string | null | undefined, lane: {
|
|
161
|
-
session: string;
|
|
162
|
-
pane: string | null;
|
|
163
|
-
cli_kind?: string;
|
|
164
|
-
role?: string;
|
|
165
|
-
}): Promise<WorkItem | null>;
|
|
166
|
-
/**
|
|
167
|
-
* The bind decision AFTER attempting to adopt the spawn's referenced task. PURE
|
|
168
|
-
* over the adopt result + whether the referenced row exists, so the
|
|
169
|
-
* "a spawn that cannot bind fails loudly rather than orphaning" invariant
|
|
170
|
-
* (`absent-input-reads-as-success`) is unit-testable without standing up the
|
|
171
|
-
* ORPC handler. `bindLane` calls `adoptAssignmentTask` then this, then acts.
|
|
172
|
-
*
|
|
173
|
-
* `use` — adopt succeeded; bind the lane to that task.
|
|
174
|
-
* `create` — no task to adopt: an exploratory lane (no workItemId) or the
|
|
175
|
-
* referenced id is a PROJECT the task is created under.
|
|
176
|
-
* `refuse` — the spawn named a workItemId the lane should adopt, but NO task
|
|
177
|
-
* exists with that id. Silently creating a NEW task here would
|
|
178
|
-
* abandon the spawn's referenced brief and bind the lane to an
|
|
179
|
-
* empty task it was never meant to own — exactly the orphan
|
|
180
|
-
* `absent-input-reads-as-success` forbids (a caller that stated
|
|
181
|
-
* adopt-intent reads "success" while the referenced row rots).
|
|
182
|
-
* Failing loudly surfaces the malformed spawn instead.
|
|
183
|
-
*/
|
|
184
|
-
export type BindAction = {
|
|
185
|
-
action: "use";
|
|
186
|
-
task: WorkItem;
|
|
187
|
-
} | {
|
|
188
|
-
action: "create";
|
|
189
|
-
scope: "exploratory" | "project-parent";
|
|
190
|
-
} | {
|
|
191
|
-
action: "refuse";
|
|
192
|
-
reason: "missing-referenced-task";
|
|
193
|
-
workItemId: string;
|
|
194
|
-
};
|
|
195
|
-
export declare function resolveBindAction(args: {
|
|
196
|
-
adopted: WorkItem | null;
|
|
197
|
-
workItemId: string | null;
|
|
198
|
-
/** The kind of the row at workItemId, or null when no such row exists. */
|
|
199
|
-
referencedKind: string | null;
|
|
200
|
-
}): BindAction;
|
|
201
|
-
/**
|
|
202
|
-
* Bind a task to a KNOWN session — the RESUME path. Unlike a fresh spawn (the
|
|
203
|
-
* CLI mints the session at boot, so SessionStart must adopt), a resumed/switched
|
|
204
|
-
* lane knows its session id up front (`resumeId`). So the assignment binds the
|
|
205
|
-
* task to `resumeId` directly and writes a NEW comment as the continuation brief,
|
|
206
|
-
* before emitting the spawn with `resumeId`. `readAssignedBrief(resumeId)` then
|
|
207
|
-
* returns that continuation — so `DEFAULT_CONTINUATION_PROMPT`'s job (which rode
|
|
208
|
-
* the now-retired keystroke path) moves to a row write at assign time. Also sets
|
|
209
|
-
* `assignee_id` (ownership) for the same reason as the fresh-spawn adopt.
|
|
210
|
-
*/
|
|
211
|
-
export declare function bindTaskToKnownSession(prisma: Prisma, taskId: string, lane: {
|
|
212
|
-
session: string;
|
|
213
|
-
pane: string | null;
|
|
214
|
-
cli_kind?: string;
|
|
215
|
-
role?: string;
|
|
216
|
-
}): Promise<WorkItem | null>;
|
|
217
|
-
/**
|
|
218
|
-
* The spawn payload an assignment emits. Carries the task id (as `workItemId`,
|
|
219
|
-
* which the picker exports into the pane so SessionStart can ADOPT the existing
|
|
220
|
-
* task) and NEVER an `initialPrompt` — the lane pulls its brief from the DB by
|
|
221
|
-
* its own session id. This is the contract whatever host-command surface lands:
|
|
222
|
-
* command `spawn_agent_tab`, payload `{ workItemId, slug?, windowName?,
|
|
223
|
-
* idempotencyKey? }`.
|
|
224
|
-
*
|
|
225
|
-
* Pure (no I/O) so the "no keystroke brief, taskId-only" invariant is
|
|
226
|
-
* unit-testable and regression-locked: a test asserting `initialPrompt` is
|
|
227
|
-
* absent fails the moment someone re-adds keystroke delivery.
|
|
228
|
-
*/
|
|
229
|
-
export declare function buildAssignmentSpawnPayload(task: Pick<WorkItem, "id">, options: {
|
|
230
|
-
slug?: string;
|
|
231
|
-
windowName?: string;
|
|
232
|
-
idempotencyKey?: string;
|
|
233
|
-
}): {
|
|
234
|
-
command: "spawn_agent_tab";
|
|
235
|
-
payload: {
|
|
236
|
-
workItemId: string;
|
|
237
|
-
slug?: string;
|
|
238
|
-
windowName?: string;
|
|
239
|
-
idempotencyKey?: string;
|
|
240
|
-
};
|
|
241
|
-
};
|
|
242
|
-
/**
|
|
243
|
-
* The emission surface an assignment writes the spawn intent to. Injected (not
|
|
244
|
-
* imported) so this file composes with whichever bus surface the host-command-bus
|
|
245
|
-
* cutover settles on, rather than hard-coding a second mechanism. Implementations
|
|
246
|
-
* write a durable row the consumer reacts to (`actions-are-rows-then-emissions`):
|
|
247
|
-
* fire-and-forget from the caller's perspective — the caller states intent and
|
|
248
|
-
* proceeds; the consumer spawns the lane and the lane self-briefs.
|
|
249
|
-
*/
|
|
250
|
-
export interface AssignmentEmitter {
|
|
251
|
-
/** Write the spawn intent as a durable row the host-command consumer reacts to. */
|
|
252
|
-
emitSpawn(payload: ReturnType<typeof buildAssignmentSpawnPayload>): Promise<void>;
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* The outcome of assigning a task: the created/confirmed task and confirmation
|
|
256
|
-
* that the spawn intent was emitted (not that the lane spawned — that is the
|
|
257
|
-
* consumer's reaction, observed separately). The caller proceeds on the row.
|
|
258
|
-
*/
|
|
259
|
-
export interface AssignmentResult {
|
|
260
|
-
task: WorkItem;
|
|
261
|
-
emitted: boolean;
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* createBriefTask ORPC input. The MINT entry point a spawner calls to turn a
|
|
265
|
-
* raw brief into the task row a lane self-briefs from. `name` is the lane's work
|
|
266
|
-
* label (the spawner's windowName); `description` is the brief, stored as the
|
|
267
|
-
* task's FIRST COMMENT — never on the task row itself, so "the brief" has
|
|
268
|
-
* exactly one home (`task-sync-brief.ts` readLatestTaskComment, newest-first).
|
|
269
|
-
* Mirrors `assignTaskInput` minus the spawn-shaping fields (slug/windowName/
|
|
270
|
-
* idempotencyKey), which belong on the spawn emission, not the mint.
|
|
271
|
-
*/
|
|
272
|
-
export declare const createBriefTaskInput: v.ObjectSchema<{
|
|
273
|
-
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "name is required — the lane's real work label">, v.CheckAction<string, "name must carry the lane's real work name, not the generic sentinel 'Lane'">]>;
|
|
274
|
-
readonly description: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "description is required — it is the brief (first comment)">]>;
|
|
275
|
-
readonly parent_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
276
|
-
readonly organisation_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
277
|
-
}, undefined>;
|
|
278
|
-
/**
|
|
279
|
-
* createBriefTask ORPC handler. `spawn_agent_tab` calls this when it receives
|
|
280
|
-
* an `initialPrompt` with no `workItemId`: the prompt becomes the task's first
|
|
281
|
-
* comment, the returned `work_item_id` rides the spawn payload as `workItemId`,
|
|
282
|
-
* and the lane PULLS the brief at boot (`readAssignedBrief`) — no keystroke, no
|
|
283
|
-
* channel send, no positional argument. That deletes the keystroke-brief
|
|
284
|
-
* failure class by construction (`buildAssignmentSpawnPayload` already carries
|
|
285
|
-
* `workItemId` and never `initialPrompt`).
|
|
286
|
-
*
|
|
287
|
-
* Wraps the pure `createTaskWithBriefComment` (atomic task + first comment). The
|
|
288
|
-
* task is `kind:"task"`, `status:"active"`, `assignee_type:"agent"`, with NO
|
|
289
|
-
* `kind_data.lane.session`: the CLI mints the session at spawn and SessionStart
|
|
290
|
-
* ADOPTS this task by id (`adoptAssignmentTask`), which binds the brief to the
|
|
291
|
-
* lane. Returns only the `work_item_id` — the single fact the spawner needs to
|
|
292
|
-
* carry the brief without writing it into the spawn payload. Same transport as
|
|
293
|
-
* `readAssignedBrief`: page-gate-exempt (fn.taskSync), localhost super-admin.
|
|
294
|
-
*/
|
|
295
|
-
export declare const createBriefTaskHandler: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
|
|
296
|
-
headers: Headers;
|
|
297
|
-
resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
|
|
298
|
-
prisma: any;
|
|
299
|
-
user: import("./auth-router.ts").User;
|
|
300
|
-
orgId: string | null | undefined;
|
|
301
|
-
}>, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
|
|
302
|
-
work_item_id: string;
|
|
303
|
-
}, {
|
|
304
|
-
work_item_id: string;
|
|
305
|
-
}>, Record<never, never>, Record<never, never>>;
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Lane BRIEF resolution for task-sync: a spawned lane PULLS its own brief. The
|
|
7
|
-
* lane's CLI extension reads its own session id on load, resolves the task
|
|
8
|
-
* assigned to that session, and reads the brief from that task's LATEST comment.
|
|
9
|
-
* Nothing is pasted, typed, passed as an argument or sent over a channel
|
|
10
|
-
* (`scala-docs/reference/action-emission-architecture.md` "The lane PULLS its
|
|
11
|
-
* brief"). This file owns the readAssignedBrief ORPC handler + its pure helper;
|
|
12
|
-
* the router mounts the handler.
|
|
13
|
-
*
|
|
14
|
-
* Split out of task-sync-router.ts for the same reason task-sync-lane.ts +
|
|
15
|
-
* task-sync-projection.ts exist: the router sits at its 1000-line commit gate,
|
|
16
|
-
* so every cohesive unit that can live in a sibling does. The self-brief leg is
|
|
17
|
-
* one unit — the handler + the response-shaping decision belong together.
|
|
18
|
-
*
|
|
19
|
-
* CAPABILITY HARVEST (capability-harvest-on-split): APP-LOCAL, not a capability
|
|
20
|
-
* candidate. Bound to the work_items + comments schema and to task-sync's own
|
|
21
|
-
* kind_data.lane.session convention; no unrelated app could install this
|
|
22
|
-
* unmodified.
|
|
23
|
-
*/
|
|
24
|
-
import type { Prisma, CommentRow } from "./task-sync-router.ts";
|
|
25
|
-
import { type Context } from "./auth-router.ts";
|
|
26
|
-
/**
|
|
27
|
-
* The outcome of a brief read, single-sourced here so every CLI's self-brief
|
|
28
|
-
* hook (zerostack/codex shell, opencode/pi TS) branches on ONE field rather
|
|
29
|
-
* than each re-deriving "is the brief missing" from null/empty probes — the
|
|
30
|
-
* seam at which absent-input-reads-as-success hid (`the-severe-bugs-live-at-seams`).
|
|
31
|
-
*
|
|
32
|
-
* The hook decides ACTION from this; the classification lives here. A fleet
|
|
33
|
-
* lane (SCALA_LANE_WORK_ITEM set) treats anything but "ready" as a hard halt.
|
|
34
|
-
*/
|
|
35
|
-
export type BriefReadStatus = "ready" | "empty_brief" | "no_comment" | "unbound";
|
|
36
|
-
/**
|
|
37
|
-
* Shape the readAssignedBrief response from a resolved task + its latest
|
|
38
|
-
* comment. Pure (no prisma) so the decision — "no task/comment ⇒ nulls, never a
|
|
39
|
-
* verdict from no data" (`no-verdict-from-no-data`) — is unit-testable.
|
|
40
|
-
*
|
|
41
|
-
* `work_item_id` is set iff the TASK resolved (independent of whether a comment
|
|
42
|
-
* exists), and `status` distinguishes the four states a hook must act on:
|
|
43
|
-
* - "ready" — task + a non-empty comment: inject the brief.
|
|
44
|
-
* - "empty_brief" — task resolved, comment exists but content is blank: the
|
|
45
|
-
* spawn minted a task with no usable brief — broken.
|
|
46
|
-
* - "no_comment" — task resolved, no comment at all: same broken class.
|
|
47
|
-
* - "unbound" — no task resolved: the lane has no brief task (a non-fleet
|
|
48
|
-
* operator tab, or — for the session-only path — a bind
|
|
49
|
-
* that has not yet commit; the work_item_id path cannot
|
|
50
|
-
* reach this for a fleet lane).
|
|
51
|
-
*
|
|
52
|
-
* "empty_brief" / "no_comment" with the task resolved is the state the
|
|
53
|
-
* absent-input fix exists to catch: a fleet lane whose spawn succeeded MUST
|
|
54
|
-
* have a non-empty brief comment, so its absence is a defect the hook halts on
|
|
55
|
-
* rather than reading as success.
|
|
56
|
-
*/
|
|
57
|
-
export declare function shapeBriefResponse(task: {
|
|
58
|
-
id: string;
|
|
59
|
-
} | null, comment: Pick<CommentRow, "content" | "created_at"> | null): {
|
|
60
|
-
work_item_id: string | null;
|
|
61
|
-
brief: string | null;
|
|
62
|
-
comment_created_at: string | null;
|
|
63
|
-
status: BriefReadStatus;
|
|
64
|
-
};
|
|
65
|
-
/**
|
|
66
|
-
* Read the LATEST comment on a work_items task. The comment IS the brief for a
|
|
67
|
-
* run: every spawn writes a NEW comment, and the description is never the brief,
|
|
68
|
-
* so "what was this lane told" always has exactly one answer
|
|
69
|
-
* (`assignment-as-dispatch`). Newest-first by created_at. Returns null when the
|
|
70
|
-
* task has no comment yet — the extension treats that as "no brief to inject".
|
|
71
|
-
*/
|
|
72
|
-
export declare function readLatestTaskComment(prisma: Prisma, workItemId: string): Promise<CommentRow | null>;
|
|
73
|
-
/**
|
|
74
|
-
* readAssignedBrief ORPC handler. A lane's CLI extension calls this with its
|
|
75
|
-
* own session id to pull its brief. Same transport as bindLane/readTaskList: the
|
|
76
|
-
* session id arrives on x-lane-session (and body.lane_session as fallback). The
|
|
77
|
-
* procedure is page-gate-exempt (fn.taskSync), so a localhost pane shell
|
|
78
|
-
* authenticates as super-admin via resolveUser with no session cookie.
|
|
79
|
-
*/
|
|
80
|
-
export declare const readAssignedBriefHandler: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
|
|
81
|
-
headers: Headers;
|
|
82
|
-
resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
|
|
83
|
-
prisma: any;
|
|
84
|
-
user: import("./auth-router.ts").User;
|
|
85
|
-
orgId: string | null | undefined;
|
|
86
|
-
}>, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
|
|
87
|
-
work_item_id: string | null;
|
|
88
|
-
brief: string | null;
|
|
89
|
-
comment_created_at: string | null;
|
|
90
|
-
status: BriefReadStatus;
|
|
91
|
-
}, {
|
|
92
|
-
work_item_id: string | null;
|
|
93
|
-
brief: string | null;
|
|
94
|
-
comment_created_at: string | null;
|
|
95
|
-
status: BriefReadStatus;
|
|
96
|
-
}>, Record<never, never>, Record<never, never>>;
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* DB→AGENT dispatch for task-sync — notifies a lane's live pane that its
|
|
7
|
-
* work_items task list changed out-of-band (operator toggled a subtask in the
|
|
8
|
-
* projects UI, another lane edited a shared task). The pane's adapter then
|
|
9
|
-
* re-reads (fn.taskSync.readTaskList) and updates its native list.
|
|
10
|
-
*
|
|
11
|
-
* MECHANISM — reuses the existing CLI-agnostic channel delivery, no new
|
|
12
|
-
* transport: a POST to scala-channel's fleet-events publish entry
|
|
13
|
-
* (POST /api/fleet/events — @teamscala/channel/fleet-events-publish) carrying an
|
|
14
|
-
* `orchestrator.message` FleetEvent with slotName "cross-pane". That slot is
|
|
15
|
-
* the watchEvents bypass for events NOT tied to a fleet_agent_runs row
|
|
16
|
-
* (FleetEventOrchestratorMessageSchema comment) — exactly the system-originated
|
|
17
|
-
* case. scala-channel re-emits onto its eventBus and streamFleetEvents delivers
|
|
18
|
-
* to the subscriber filtered by orchestratorSessionId (the target lane pane).
|
|
19
|
-
* Both the Claude Code relay and the OpenCode scala-hooks plugin subscribe to
|
|
20
|
-
* that SSE stream for their own pane identity and inject each event natively —
|
|
21
|
-
* so ONE publish reaches every CLI that has a live projection, no per-CLI
|
|
22
|
-
* dispatch code here.
|
|
23
|
-
*
|
|
24
|
-
* NOT the dispatch entry (POST /api/channel/send): that path requires a real
|
|
25
|
-
* sender pane identity (channel-sender-identity-validated anti-spoof) and so
|
|
26
|
-
* cannot carry a system-originated notification. The publish path is
|
|
27
|
-
* internal-trusted (localhost super-admin) and accepts a synthetic
|
|
28
|
-
* fromOrchestratorId — the correct path for a system→pane message.
|
|
29
|
-
*
|
|
30
|
-
* scala-channel's address is resolved from its service_routing row (local_port)
|
|
31
|
-
* — never hardcoded (service-ports-from-registry). Fire-and-forget: a dispatch
|
|
32
|
-
* failure (scala-channel down, port unresolvable) is logged and swallowed; it
|
|
33
|
-
* must never block the work_items write that triggered it, and the lane's
|
|
34
|
-
* SessionStart/UserPromptSubmit backstop picks up the change on its next turn
|
|
35
|
-
* regardless.
|
|
36
|
-
*/
|
|
37
|
-
/**
|
|
38
|
-
* The `orchestrator.message` FleetEvent payload that notifies a lane its task
|
|
39
|
-
* list changed. Pure (no I/O) so it is unit-testable; notifyLaneOfTaskChange
|
|
40
|
-
* posts it. slotName "cross-pane" is REQUIRED — it is the watchEvents bypass for
|
|
41
|
-
* events not tied to a fleet_agent_runs row. orchestratorSessionId is the TARGET
|
|
42
|
-
* lane pane (the SSE subscriber filter). The message is single-line (the
|
|
43
|
-
* <channel> tag renders it inline).
|
|
44
|
-
*/
|
|
45
|
-
export interface TaskChangedEvent {
|
|
46
|
-
type: "orchestrator.message";
|
|
47
|
-
message: string;
|
|
48
|
-
fromOrchestratorId: string;
|
|
49
|
-
runId: string;
|
|
50
|
-
slotName: "cross-pane";
|
|
51
|
-
ts: number;
|
|
52
|
-
orchestratorSessionId: string;
|
|
53
|
-
}
|
|
54
|
-
export declare function buildTaskChangedEvent(lanePane: string, taskId: string, now?: number): TaskChangedEvent;
|
|
55
|
-
/**
|
|
56
|
-
* Publish a "your task list changed" notification to the owning lane's pane.
|
|
57
|
-
* The pane identity is the stable tmux id ("tmux:%42") every CLI relay/plugin
|
|
58
|
-
* receives for. `taskId` is carried in the message so the recipient can re-read
|
|
59
|
-
* just that task. Fire-and-forget.
|
|
60
|
-
*/
|
|
61
|
-
export declare function notifyLaneOfTaskChange(lanePane: string, taskId: string): Promise<void>;
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* DB→AGENT dispatch for task-sync — notifies a lane's live pane that its
|
|
7
|
-
* work_items task list changed out-of-band (operator toggled a subtask in the
|
|
8
|
-
* projects UI, another lane edited a shared task). The pane's adapter then
|
|
9
|
-
* re-reads (fn.taskSync.readTaskList) and updates its native list.
|
|
10
|
-
*
|
|
11
|
-
* MECHANISM — reuses the existing CLI-agnostic channel delivery, no new
|
|
12
|
-
* transport: a POST to scala-channel's fleet-events publish entry
|
|
13
|
-
* (POST /api/fleet/events — @teamscala/channel/fleet-events-publish) carrying an
|
|
14
|
-
* `orchestrator.message` FleetEvent with slotName "cross-pane". That slot is
|
|
15
|
-
* the watchEvents bypass for events NOT tied to a fleet_agent_runs row
|
|
16
|
-
* (FleetEventOrchestratorMessageSchema comment) — exactly the system-originated
|
|
17
|
-
* case. scala-channel re-emits onto its eventBus and streamFleetEvents delivers
|
|
18
|
-
* to the subscriber filtered by orchestratorSessionId (the target lane pane).
|
|
19
|
-
* Both the Claude Code relay and the OpenCode scala-hooks plugin subscribe to
|
|
20
|
-
* that SSE stream for their own pane identity and inject each event natively —
|
|
21
|
-
* so ONE publish reaches every CLI that has a live projection, no per-CLI
|
|
22
|
-
* dispatch code here.
|
|
23
|
-
*
|
|
24
|
-
* NOT the dispatch entry (POST /api/channel/send): that path requires a real
|
|
25
|
-
* sender pane identity (channel-sender-identity-validated anti-spoof) and so
|
|
26
|
-
* cannot carry a system-originated notification. The publish path is
|
|
27
|
-
* internal-trusted (localhost super-admin) and accepts a synthetic
|
|
28
|
-
* fromOrchestratorId — the correct path for a system→pane message.
|
|
29
|
-
*
|
|
30
|
-
* scala-channel's address is resolved from its service_routing row (local_port)
|
|
31
|
-
* — never hardcoded (service-ports-from-registry). Fire-and-forget: a dispatch
|
|
32
|
-
* failure (scala-channel down, port unresolvable) is logged and swallowed; it
|
|
33
|
-
* must never block the work_items write that triggered it, and the lane's
|
|
34
|
-
* SessionStart/UserPromptSubmit backstop picks up the change on its next turn
|
|
35
|
-
* regardless.
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
39
|
-
import { ServiceRoutingConfigSchema } from "@teamscala/db-validation/registry-schemas/service-routing";
|
|
40
|
-
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
41
|
-
|
|
42
|
-
let cachedChannelBaseUrl: string | null | undefined;
|
|
43
|
-
|
|
44
|
-
async function resolveChannelBaseUrl(): Promise<string | null> {
|
|
45
|
-
if (cachedChannelBaseUrl !== undefined) return cachedChannelBaseUrl;
|
|
46
|
-
try {
|
|
47
|
-
const row = await loadRegistryConfig("service_routing", "scala-channel", ServiceRoutingConfigSchema);
|
|
48
|
-
const port = row?.local_port;
|
|
49
|
-
cachedChannelBaseUrl = port ? `http://127.0.0.1:${port}` : null;
|
|
50
|
-
} catch {
|
|
51
|
-
cachedChannelBaseUrl = null;
|
|
52
|
-
}
|
|
53
|
-
return cachedChannelBaseUrl;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** The system sender identity stamped on fromOrchestratorId. */
|
|
57
|
-
const FROM_ORCHESTRATOR_ID = "task-sync";
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* The `orchestrator.message` FleetEvent payload that notifies a lane its task
|
|
61
|
-
* list changed. Pure (no I/O) so it is unit-testable; notifyLaneOfTaskChange
|
|
62
|
-
* posts it. slotName "cross-pane" is REQUIRED — it is the watchEvents bypass for
|
|
63
|
-
* events not tied to a fleet_agent_runs row. orchestratorSessionId is the TARGET
|
|
64
|
-
* lane pane (the SSE subscriber filter). The message is single-line (the
|
|
65
|
-
* <channel> tag renders it inline).
|
|
66
|
-
*/
|
|
67
|
-
export interface TaskChangedEvent {
|
|
68
|
-
type: "orchestrator.message";
|
|
69
|
-
message: string;
|
|
70
|
-
fromOrchestratorId: string;
|
|
71
|
-
runId: string;
|
|
72
|
-
slotName: "cross-pane";
|
|
73
|
-
ts: number;
|
|
74
|
-
orchestratorSessionId: string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function buildTaskChangedEvent(lanePane: string, taskId: string, now: number = Date.now()): TaskChangedEvent {
|
|
78
|
-
return {
|
|
79
|
-
type: "orchestrator.message",
|
|
80
|
-
message: `TaskSync: your work_items task list changed externally (task ${taskId}). Re-read via fn.taskSync.readTaskList and update your todo list.`,
|
|
81
|
-
fromOrchestratorId: FROM_ORCHESTRATOR_ID,
|
|
82
|
-
runId: `task-sync-${taskId}-${now}`,
|
|
83
|
-
slotName: "cross-pane",
|
|
84
|
-
ts: now,
|
|
85
|
-
orchestratorSessionId: lanePane,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Publish a "your task list changed" notification to the owning lane's pane.
|
|
91
|
-
* The pane identity is the stable tmux id ("tmux:%42") every CLI relay/plugin
|
|
92
|
-
* receives for. `taskId` is carried in the message so the recipient can re-read
|
|
93
|
-
* just that task. Fire-and-forget.
|
|
94
|
-
*/
|
|
95
|
-
export async function notifyLaneOfTaskChange(
|
|
96
|
-
lanePane: string,
|
|
97
|
-
taskId: string,
|
|
98
|
-
): Promise<void> {
|
|
99
|
-
const log = getAppLogger();
|
|
100
|
-
if (!lanePane) return;
|
|
101
|
-
const base = await resolveChannelBaseUrl();
|
|
102
|
-
if (!base) {
|
|
103
|
-
log.warn("[task-sync] notifyLaneOfTaskChange: scala-channel address unresolvable", { taskId });
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
const payload = buildTaskChangedEvent(lanePane, taskId);
|
|
107
|
-
try {
|
|
108
|
-
const res = await fetch(`${base}/api/fleet/events`, {
|
|
109
|
-
method: "POST",
|
|
110
|
-
headers: { "content-type": "application/json" },
|
|
111
|
-
body: JSON.stringify(payload),
|
|
112
|
-
signal: AbortSignal.timeout(3000),
|
|
113
|
-
});
|
|
114
|
-
if (!res.ok) {
|
|
115
|
-
log.warn("[task-sync] notifyLaneOfTaskChange: non-2xx from scala-channel", {
|
|
116
|
-
taskId, status: res.status,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
} catch (err) {
|
|
120
|
-
log.warn("[task-sync] notifyLaneOfTaskChange: POST failed", {
|
|
121
|
-
taskId, error: err instanceof Error ? err.message : String(err),
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Lane RESOLUTION for task-sync: given a session or a pane, which work_items
|
|
7
|
-
* task IS this lane, and what is it called.
|
|
8
|
-
*
|
|
9
|
-
* Split out of task-sync-router.ts, which had reached 998 of its 1000-line
|
|
10
|
-
* budget — two lines from frozen. That is not tidiness: the budget is a commit
|
|
11
|
-
* gate, so at 998 lines ANY change to task-sync was blocked for every lane, and
|
|
12
|
-
* the four-line fix that needed to land could not. The router header records
|
|
13
|
-
* being split once before for exactly this reason; it grew back.
|
|
14
|
-
*
|
|
15
|
-
* The seam matches the one that split the projection. Everything here answers
|
|
16
|
-
* "which lane, called what" from an identity, and none of it touches the ORPC
|
|
17
|
-
* surface or decides what a task list should become — which is why these move
|
|
18
|
-
* together rather than one at a time: resolveLaneTask alone has seven callers.
|
|
19
|
-
*
|
|
20
|
-
* CAPABILITY HARVEST (capability-harvest-on-split): APP-LOCAL, not a capability
|
|
21
|
-
* candidate. Checked against the 14 platform singletons and the package
|
|
22
|
-
* catalogue; the doctrine test fails plainly, because every export is bound to
|
|
23
|
-
* the work_items schema and to task-sync own kind_data.lane convention, so no
|
|
24
|
-
* unrelated app could install this unmodified.
|
|
25
|
-
*/
|
|
26
|
-
import type { Prisma, WorkItem } from "./task-sync-router.ts";
|
|
27
|
-
export declare function resolveLaneWorkName(prisma: Prisma, pane: string): Promise<string | null>;
|
|
28
|
-
export declare function resolveLaneTask(prisma: Prisma, session: string): Promise<WorkItem | null>;
|
|
29
|
-
/**
|
|
30
|
-
* Resolve a lane's task by its AUTHORITATIVE work_item id — the id the spawn
|
|
31
|
-
* minted (createBriefTask) and exported into the pane as SCALA_LANE_WORK_ITEM.
|
|
32
|
-
*
|
|
33
|
-
* WHY this exists alongside the session resolution: readAssignedBrief used to
|
|
34
|
-
* resolve ONLY by kind_data.lane.session, which is written by the lane's OWN
|
|
35
|
-
* bindLane hook at boot. That bind is a fire-and-forget POST racing the brief
|
|
36
|
-
* read, so a fleet lane whose bind had not yet commit read "no task" and the
|
|
37
|
-
* self-brief hook silently exited 0 — absent-input-reads-as-success at the
|
|
38
|
-
* dispatch boundary (a lane booted, was told its brief was loaded, had none,
|
|
39
|
-
* ran nothing, and was archived having done nothing — measured 2026-08-02).
|
|
40
|
-
*
|
|
41
|
-
* The work_item id is known to the spawn before the lane even starts
|
|
42
|
-
* (mintBriefTask returns it; the spawn REFUSES if minting fails for a capture
|
|
43
|
-
* CLI), so resolving by it CANNOT race: the row exists because the spawn
|
|
44
|
-
* created it. The session bind stays for task-list writes (upsertFromTaskList)
|
|
45
|
-
* — it is no longer load-bearing for brief delivery. Any status is matched
|
|
46
|
-
* (active OR archived) because the brief is the brief regardless of the run's
|
|
47
|
-
* state — closing a run archives the task, not the brief.
|
|
48
|
-
*/
|
|
49
|
-
export declare function resolveTaskById(prisma: Prisma, workItemId: string): Promise<WorkItem | null>;
|