@teamscala/service-boot 0.23.334 → 0.23.336

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.
Files changed (30) hide show
  1. package/package.json +1088 -1089
  2. package/src/auth-gate.ts +2 -3
  3. package/src/build-client-assets.test.ts +1 -1
  4. package/src/build-client-assets.ts +1 -1
  5. package/src/fn-routers/orchestrator-registration-router.d.ts +17 -3
  6. package/src/fn-routers/orchestrator-registration-router.ts +17 -3
  7. package/src/fn-routers/orchestrator-registration.live.test.ts +23 -2
  8. package/src/fn-routers/orchestrator-registration.test.ts +23 -0
  9. package/src/fn-routers/task-sync-assign.test.ts +1 -0
  10. package/src/fn-routers/task-sync-bind.test.ts +18 -18
  11. package/src/fn-routers/task-sync-brief.d.ts +7 -8
  12. package/src/fn-routers/task-sync-brief.test.ts +10 -12
  13. package/src/fn-routers/task-sync-brief.ts +9 -10
  14. package/src/fn-routers/task-sync-dispatch.d.ts +40 -54
  15. package/src/fn-routers/task-sync-dispatch.test.ts +51 -33
  16. package/src/fn-routers/task-sync-dispatch.ts +57 -100
  17. package/src/fn-routers/task-sync-lane.test.ts +36 -0
  18. package/src/fn-routers/task-sync-lane.ts +8 -19
  19. package/src/fn-routers/task-sync-projection.d.ts +23 -0
  20. package/src/fn-routers/task-sync-projection.test.ts +26 -0
  21. package/src/fn-routers/task-sync-projection.ts +51 -0
  22. package/src/fn-routers/task-sync-router.d.ts +30 -23
  23. package/src/fn-routers/task-sync-router.test.ts +14 -2
  24. package/src/fn-routers/task-sync-router.ts +72 -114
  25. package/src/handlers/sweep-host-cache.d.ts +21 -1
  26. package/src/handlers/sweep-host-cache.test.ts +85 -0
  27. package/src/handlers/sweep-host-cache.ts +118 -4
  28. package/src/mount-orpc-router.ts +1 -1
  29. package/src/resolve-brand-favicon.d.ts +1 -1
  30. package/src/resolve-brand-favicon.ts +1 -1
@@ -3,122 +3,79 @@
3
3
  * @status handwritten
4
4
  * @edit edit directly
5
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.
6
+ * DB→AGENT task projection emission. A work_items change is delivered by one
7
+ * canonical comments row addressed to the active cli_sessions runtime; the
8
+ * database emission wakes that runtime adapter, which re-reads readTaskList.
9
+ * This module owns no pane transport, terminal fallback, or second message
10
+ * queue.
36
11
  */
37
12
 
38
- import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
39
- import { ServiceRoutingConfigSchema } from "@teamscala/db-validation/registry-schemas/service-routing";
40
13
  import { getAppLogger } from "@teamscala/logger/app-loggers";
41
14
 
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;
15
+ export interface TaskChangeComment {
16
+ work_item_id: string;
17
+ content: string;
18
+ system_key: string;
19
+ source_cli_session_id: string;
20
+ sender_role: "system";
21
+ sender_name: "TaskSync";
54
22
  }
55
23
 
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;
24
+ export function buildTaskChangeComment(
25
+ taskId: string,
26
+ sessionId: string,
27
+ eventKey: string = "refresh",
28
+ ): TaskChangeComment {
29
+ return {
30
+ work_item_id: taskId,
31
+ content: `TaskSync: work_items task ${taskId} changed; re-read fn.taskSync.readTaskList.`,
32
+ system_key: `task-sync:${taskId}:${sessionId}:${eventKey}`,
33
+ source_cli_session_id: sessionId,
34
+ sender_role: "system",
35
+ sender_name: "TaskSync",
36
+ };
75
37
  }
76
38
 
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,
39
+ export interface TaskDispatchPrisma {
40
+ cli_sessions: {
41
+ findFirst(args: unknown): Promise<{ id: string } | null>;
42
+ };
43
+ comments: {
44
+ upsert(args: {
45
+ where: { work_item_id_system_key: { work_item_id: string; system_key: string } };
46
+ create: TaskChangeComment;
47
+ update: Record<string, never>;
48
+ }): Promise<{ id: string }>;
86
49
  };
87
50
  }
88
51
 
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
- */
52
+ /** Write the durable task-change comment for the active runtime, if present. */
95
53
  export async function notifyLaneOfTaskChange(
96
- lanePane: string,
54
+ prisma: TaskDispatchPrisma,
97
55
  taskId: string,
98
- ): Promise<void> {
56
+ eventKey: string = "refresh",
57
+ ): Promise<{ delivered: boolean; sessionId: string | null; commentId: string | null }> {
99
58
  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);
59
+ const session = await prisma.cli_sessions.findFirst({
60
+ where: { work_item_id: taskId, closed_at: null, lifecycle_state: { not: "closed" } },
61
+ orderBy: { created_at: "desc" },
62
+ select: { id: true },
63
+ });
64
+ if (!session) return { delivered: false, sessionId: null, commentId: null };
107
65
  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),
66
+ const data = buildTaskChangeComment(taskId, session.id, eventKey);
67
+ const comment = await prisma.comments.upsert({
68
+ where: { work_item_id_system_key: { work_item_id: taskId, system_key: data.system_key } },
69
+ create: data,
70
+ update: {},
113
71
  });
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),
72
+ return { delivered: true, sessionId: session.id, commentId: comment.id };
73
+ } catch (error) {
74
+ log.warn("[task-sync] durable task comment emission failed", {
75
+ taskId,
76
+ sessionId: session.id,
77
+ error: error instanceof Error ? error.message : String(error),
122
78
  });
79
+ return { delivered: false, sessionId: session.id, commentId: null };
123
80
  }
124
81
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Behavioral tests for canonical CLI-session lane resolution.
7
+ */
8
+ import { describe, expect, it } from "bun:test";
9
+ import { resolveLaneWorkName } from "./task-sync-lane.ts";
10
+
11
+ describe("resolveLaneWorkName", () => {
12
+ it("reads the visible label from the active cli_sessions route", async () => {
13
+ let query: unknown;
14
+ const name = await resolveLaneWorkName(
15
+ {
16
+ cli_sessions: {
17
+ findFirst: async (args: unknown) => {
18
+ query = args;
19
+ return { id: "session-1", label: "ZeroStack" };
20
+ },
21
+ },
22
+ } as never,
23
+ "tmux:%7",
24
+ );
25
+ expect(name).toBe("ZeroStack");
26
+ expect(query).toMatchObject({ where: { host_route: { path: ["pane_id"], equals: "tmux:%7" } } });
27
+ });
28
+
29
+ it("returns no unverified title when no active session owns the pane", async () => {
30
+ const name = await resolveLaneWorkName(
31
+ { cli_sessions: { findFirst: async () => null } } as never,
32
+ "tmux:%8",
33
+ );
34
+ expect(name).toBeNull();
35
+ });
36
+ });
@@ -28,27 +28,16 @@ import type { Prisma, WorkItem } from "./task-sync-router.ts";
28
28
 
29
29
  export async function resolveLaneWorkName(prisma: Prisma, pane: string): Promise<string | null> {
30
30
  try {
31
- const runs = (
32
- prisma as unknown as {
33
- fleet_agent_runs?: {
34
- findFirst: (a: unknown) => Promise<{ slot_name?: string | null } | null>;
35
- };
36
- }
37
- ).fleet_agent_runs;
38
- if (!runs?.findFirst) return null;
39
- // Most recent run for the pane, REGARDLESS of state. A lane's work name does
40
- // not vanish when its run closes, and filtering `state != closed` here is
41
- // exactly why task rows were titled "Lane": a task-list write (ensureLaneTask)
42
- // that fires after the run closed resolved no name and fell back to the
43
- // generic sentinel — spawn debris an orchestrator cannot attribute
44
- // (measured 2026-08-01: 207 "Lane" rows). Newest-first so a reused pane
45
- // resolves its CURRENT lane, not a prior one.
46
- const run = await runs.findFirst({
47
- where: { pane },
31
+ const session = await prisma.cli_sessions.findFirst({
32
+ where: {
33
+ closed_at: null,
34
+ lifecycle_state: { not: "closed" },
35
+ host_route: { path: ["pane_id"], equals: pane },
36
+ },
48
37
  orderBy: { created_at: "desc" },
49
- select: { slot_name: true },
38
+ select: { id: true, label: true },
50
39
  });
51
- const name = run?.slot_name?.trim();
40
+ const name = session?.label?.trim();
52
41
  return name ? name : null;
53
42
  } catch {
54
43
  return null;
@@ -33,6 +33,29 @@ import type { TaskListItem, WorkItem } from "./task-sync-router.ts";
33
33
  /** CLI task-list status → work_items.status. The canonical forward direction. */
34
34
  export declare const STATUS_TO_WORK_ITEM: Record<"completed" | "in_progress" | "pending", string>;
35
35
  export declare function normalizeStatus(s: string | null): TaskListItem["status"];
36
+ export interface ProgressSubtask {
37
+ id: string;
38
+ status: string | null;
39
+ kind_data?: Record<string, unknown> | null;
40
+ }
41
+ export interface ProgressAcceptanceEvidence {
42
+ work_item_id: string | null;
43
+ sender_role: string | null;
44
+ system_key: string | null;
45
+ }
46
+ export interface EvidenceProgress {
47
+ total: number;
48
+ accepted: number;
49
+ percentage: number;
50
+ }
51
+ /**
52
+ * Derive progress from the canonical checklist and independent acceptance
53
+ * comments. A done status without an Orchestrator acceptance comment is not
54
+ * evidence of completion; prose percentages and context gauges never enter
55
+ * this calculation. Subtasks are required by default, with an explicit
56
+ * `kind_data.pm_progress.required: false` opt-out for non-gating notes.
57
+ */
58
+ export declare function deriveEvidenceProgress(subtasks: ProgressSubtask[], evidence: ProgressAcceptanceEvidence[]): EvidenceProgress;
36
59
  /**
37
60
  * Decide the status to WRITE for an existing row, given what the CLI list says.
38
61
  *
@@ -12,6 +12,7 @@
12
12
  import { describe, expect, it } from "bun:test";
13
13
  import {
14
14
  STATUS_TO_WORK_ITEM,
15
+ deriveEvidenceProgress,
15
16
  normalizeStatus,
16
17
  planReconcile,
17
18
  resolveProjectedStatus,
@@ -29,6 +30,31 @@ describe("STATUS_TO_WORK_ITEM", () => {
29
30
  });
30
31
  });
31
32
 
33
+ describe("evidence-derived task progress", () => {
34
+ it("counts only required subtasks with independent Orchestrator acceptance", () => {
35
+ const out = deriveEvidenceProgress(
36
+ [
37
+ { id: "a", status: "done" },
38
+ { id: "b", status: "done" },
39
+ { id: "c", status: "todo", kind_data: { pm_progress: { required: false } } },
40
+ ],
41
+ [
42
+ { work_item_id: "a", sender_role: "orchestrator", system_key: "acceptance:a" },
43
+ { work_item_id: "b", sender_role: "worker", system_key: "acceptance:b" },
44
+ ],
45
+ );
46
+ expect(out).toEqual({ total: 2, accepted: 1, percentage: 50 });
47
+ });
48
+
49
+ it("does not treat prose, context, or done alone as evidence", () => {
50
+ expect(
51
+ deriveEvidenceProgress([{ id: "a", status: "done" }], [
52
+ { work_item_id: "a", sender_role: "orchestrator", system_key: "progress:100" },
53
+ ]),
54
+ ).toEqual({ total: 1, accepted: 0, percentage: 0 });
55
+ });
56
+ });
57
+
32
58
  describe("status translation is a single non-drifting source", () => {
33
59
  // The forward map (CLI → work_items) is canonical; the reverse is DERIVED from
34
60
  // it (+ a closed synonym set). This locks the structural fix: the two
@@ -74,6 +74,57 @@ export function normalizeStatus(s: string | null): TaskListItem["status"] {
74
74
  return (s && WORK_ITEM_TO_STATUS[s]) || "pending";
75
75
  }
76
76
 
77
+ export interface ProgressSubtask {
78
+ id: string;
79
+ status: string | null;
80
+ kind_data?: Record<string, unknown> | null;
81
+ }
82
+
83
+ export interface ProgressAcceptanceEvidence {
84
+ work_item_id: string | null;
85
+ sender_role: string | null;
86
+ system_key: string | null;
87
+ }
88
+
89
+ export interface EvidenceProgress {
90
+ total: number;
91
+ accepted: number;
92
+ percentage: number;
93
+ }
94
+
95
+ /**
96
+ * Derive progress from the canonical checklist and independent acceptance
97
+ * comments. A done status without an Orchestrator acceptance comment is not
98
+ * evidence of completion; prose percentages and context gauges never enter
99
+ * this calculation. Subtasks are required by default, with an explicit
100
+ * `kind_data.pm_progress.required: false` opt-out for non-gating notes.
101
+ */
102
+ export function deriveEvidenceProgress(
103
+ subtasks: ProgressSubtask[],
104
+ evidence: ProgressAcceptanceEvidence[],
105
+ ): EvidenceProgress {
106
+ const required = subtasks.filter(
107
+ (s) => (s.kind_data?.pm_progress as { required?: boolean } | undefined)?.required !== false,
108
+ );
109
+ const accepted = new Set(
110
+ evidence
111
+ .filter(
112
+ (e) =>
113
+ e.sender_role === "orchestrator" &&
114
+ typeof e.system_key === "string" &&
115
+ e.system_key.startsWith("acceptance:") &&
116
+ e.work_item_id,
117
+ )
118
+ .map((e) => e.work_item_id as string),
119
+ );
120
+ const acceptedCount = required.filter((s) => s.status === "done" && accepted.has(s.id)).length;
121
+ return {
122
+ total: required.length,
123
+ accepted: acceptedCount,
124
+ percentage: required.length === 0 ? 0 : Math.round((acceptedCount / required.length) * 100),
125
+ };
126
+ }
127
+
77
128
  /**
78
129
  * Decide the status to WRITE for an existing row, given what the CLI list says.
79
130
  *
@@ -21,8 +21,8 @@
21
21
  * identity in kind_data.lane = { pane, session, cli_kind, role } so any surface
22
22
  * (fleet UI, another lane) can resolve "who owns this work" and push changes
23
23
  * back. Binding is by pane identity (the stable $TMUX_PANE), not by fleet run —
24
- * so it works before/without the fleet_agent_runs.work_item_id join (that column
25
- * is a UI convenience for the unified lane+task+subtask view, not a dependency).
24
+ * so it works without any second runtime-assignment join; the canonical runtime
25
+ * identity remains cli_sessions and the assignment remains work_items.
26
26
  *
27
27
  * These procedures are the ONLY CLI-agnostic surface an adapter calls:
28
28
  * bindLane — bind (create-or-confirm) a lane's task. SessionStart.
@@ -38,6 +38,7 @@
38
38
  */
39
39
  import { type Context } from "./auth-router.ts";
40
40
  import * as v from "valibot";
41
+ import { type ProgressAcceptanceEvidence } from "./task-sync-projection.ts";
41
42
  /** A lane's native task-list item, normalized. Every CLI adapter maps to this. */
42
43
  export interface TaskListItem {
43
44
  content: string;
@@ -104,8 +105,15 @@ export type Prisma = {
104
105
  count: number;
105
106
  }>;
106
107
  };
108
+ cli_sessions: {
109
+ findFirst: (a: unknown) => Promise<{
110
+ id: string;
111
+ label?: string | null;
112
+ } | null>;
113
+ };
107
114
  comments?: {
108
115
  findFirst: (a: unknown) => Promise<CommentRow | null>;
116
+ findMany: (a: unknown) => Promise<Array<ProgressAcceptanceEvidence>>;
109
117
  };
110
118
  };
111
119
  /** A `comments` row — only the fields readAssignedBrief touches. */
@@ -113,18 +121,6 @@ export interface CommentRow {
113
121
  content: string;
114
122
  created_at: Date;
115
123
  }
116
- /**
117
- * A dispatched run already points at its fleet_tasks row. Task-sync may fill
118
- * the convenience join for an adopted lane, but must never replace that
119
- * dispatch identity with the lane's work_items projection id.
120
- */
121
- export declare function untrackedRunForPane(pane: string): {
122
- pane: string;
123
- state: {
124
- not: string;
125
- };
126
- tracked_task_id: null;
127
- };
128
124
  /**
129
125
  * Resolve the org a lane's task is created under. Explicit when the lane knows
130
126
  * its org (a dispatched fleet lane carries one); defaults to the platform org
@@ -179,8 +175,7 @@ export declare const taskSyncRouter: {
179
175
  * adapter so the lane has a durable task row before its first todo write.
180
176
  * Idempotent: re-binding updates the identity (role/cli_kind can change) but
181
177
  * never orphans existing subtasks. After binding, links the lane's
182
- * fleet_agent_runs row (matched by pane) to the task via tracked_task_id —
183
- * the join the operator's lane-labels monitor + the unified fleet view read.
178
+ * cli_sessions is the runtime identity and work_items is the assignment.
184
179
  */
185
180
  bindLane: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
186
181
  headers: Headers;
@@ -292,18 +287,30 @@ export declare const taskSyncRouter: {
292
287
  task_id: null;
293
288
  task_title: null;
294
289
  subtasks: never[];
290
+ progress: {
291
+ total: number;
292
+ accepted: number;
293
+ percentage: number;
294
+ };
295
295
  } | {
296
296
  task_id: string;
297
297
  task_title: string;
298
298
  subtasks: TaskListItem[];
299
+ progress: import("./task-sync-projection.ts").EvidenceProgress;
299
300
  }, {
300
301
  task_id: null;
301
302
  task_title: null;
302
303
  subtasks: never[];
304
+ progress: {
305
+ total: number;
306
+ accepted: number;
307
+ percentage: number;
308
+ };
303
309
  } | {
304
310
  task_id: string;
305
311
  task_title: string;
306
312
  subtasks: TaskListItem[];
313
+ progress: import("./task-sync-projection.ts").EvidenceProgress;
307
314
  }>, Record<never, never>, Record<never, never>>;
308
315
  /**
309
316
  * DB→AGENT self-brief leg. A spawned lane PULLS its own brief — nothing is
@@ -399,11 +406,9 @@ export declare const taskSyncRouter: {
399
406
  * there the lane is the originator (no self-echo); here the lane is the
400
407
  * recipient.
401
408
  *
402
- * COMPLETION STEP (not MVP): this procedure is the explicit trigger. For
403
- * FULL coverage — every work_items subtask write, including the projects
404
- * UI's generated CRUD — a Postgres trigger + LISTEN subscriber on
405
- * scala-channel fires the same publish, so an operator edit reaches the
406
- * lane with no explicit call. Tracked in apps/agent-fleet.md task-sync.
409
+ * Every call writes the canonical task-change comment for the active
410
+ * cli_sessions row; the database comment emission is the delivery trigger.
411
+ * There is no pane transport or secondary channel publisher here.
407
412
  */
408
413
  applyExternalChange: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
409
414
  headers: Headers;
@@ -413,11 +418,13 @@ export declare const taskSyncRouter: {
413
418
  orgId: string | null | undefined;
414
419
  }>, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
415
420
  task_id: string;
416
- lane_pane: string | null;
421
+ cli_session_id: string | null;
422
+ comment_id: string | null;
417
423
  notified: boolean;
418
424
  }, {
419
425
  task_id: string;
420
- lane_pane: string | null;
426
+ cli_session_id: string | null;
427
+ comment_id: string | null;
421
428
  notified: boolean;
422
429
  }>, Record<never, never>, Record<never, never>>;
423
430
  };
@@ -337,8 +337,20 @@ describe("a blocked row survives a CLI list that cannot express it", () => {
337
337
  it("planReconcile leaves a blocked subtask blocked when the list says pending", () => {
338
338
  // The end-to-end shape: this is what re-blocked the lane three times.
339
339
  const plan = planReconcile(
340
- [{ mark: "1", content: "Deploy the fix", status: "pending" }],
341
- [{ id: "row-1", title: "Deploy the fix", status: "blocked", completed: false, position: "0", kind_data: { task_sync: { mark: "1" } } }] as never,
340
+ [
341
+ {
342
+ id: "row-1",
343
+ kind: "subtask",
344
+ parent_id: "task-1",
345
+ organisation_id: "org-1",
346
+ title: "Deploy the fix",
347
+ status: "blocked",
348
+ position: "0",
349
+ completed: false,
350
+ kind_data: { task_sync: { mark: "1" } },
351
+ },
352
+ ],
353
+ [{ content: "Deploy the fix", status: "pending" }],
342
354
  "claude-code",
343
355
  );
344
356
  const update = plan.toUpdate.find((u) => u.id === "row-1");