@teamscala/service-boot 0.23.319 → 0.23.321

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 (164) hide show
  1. package/package.json +526 -526
  2. package/src/agreement-gate.d.ts +19 -0
  3. package/src/agreement-gate.ts +0 -12
  4. package/src/asset-cache-key.d.ts +11 -0
  5. package/src/asset-optimize.d.ts +17 -0
  6. package/src/asset-proxy.d.ts +11 -0
  7. package/src/asset-social-card.d.ts +17 -0
  8. package/src/attach-agreement-magic.d.ts +20 -0
  9. package/src/attach-portal-members.d.ts +23 -0
  10. package/src/attach-proposal-accept.d.ts +26 -0
  11. package/src/attach-proposal-magic.d.ts +18 -0
  12. package/src/attach-transfers-upload.d.ts +27 -0
  13. package/src/auth-gate.d.ts +28 -0
  14. package/src/auth-gate.ts +6 -68
  15. package/src/bind-page-handler.d.ts +18 -0
  16. package/src/build-client-assets/input-hashing.d.ts +25 -0
  17. package/src/build-client-assets/source-candidates.d.ts +27 -0
  18. package/src/build-client-assets.d.ts +50 -0
  19. package/src/build-client-assets.test.ts +1 -1
  20. package/src/build-client-assets.ts +1 -1
  21. package/src/compose-page-title.d.ts +41 -0
  22. package/src/content-gate/access.d.ts +15 -0
  23. package/src/content-gate/config-types.d.ts +99 -0
  24. package/src/content-gate/gate.d.ts +13 -0
  25. package/src/content-gate/parse-cookie.d.ts +9 -0
  26. package/src/content-gate/token.d.ts +48 -0
  27. package/src/content-page-floor.d.ts +39 -0
  28. package/src/discover-blocks.d.ts +28 -0
  29. package/src/discover-blocks.ts +1 -1
  30. package/src/expand-roster.d.ts +34 -0
  31. package/src/fn-routers/agreement-completion-email.d.ts +14 -0
  32. package/src/fn-routers/agreement-send-router.d.ts +82 -0
  33. package/src/fn-routers/ai-health-router.d.ts +35 -0
  34. package/src/fn-routers/auth-router.d.ts +47 -0
  35. package/src/fn-routers/cache-admin-router.d.ts +57 -0
  36. package/src/fn-routers/chat-session-router.d.ts +219 -0
  37. package/src/fn-routers/email-defaults.d.ts +1 -0
  38. package/src/fn-routers/email-link-ttl.d.ts +1 -0
  39. package/src/fn-routers/index.d.ts +18 -0
  40. package/src/fn-routers/index.ts +0 -2
  41. package/src/fn-routers/panel-data-router.d.ts +19 -0
  42. package/src/fn-routers/persist-chat-messages.d.ts +36 -0
  43. package/src/fn-routers/plans-router.d.ts +25 -0
  44. package/src/fn-routers/proposal-send-router.d.ts +58 -0
  45. package/src/fn-routers/public-agreement-router.d.ts +289 -0
  46. package/src/fn-routers/public-assessment-router.d.ts +68 -0
  47. package/src/fn-routers/public-gpt-session-router.d.ts +143 -0
  48. package/src/fn-routers/sign-in-flow-router.d.ts +107 -0
  49. package/src/fn-routers/task-sync-assign.d.ts +305 -0
  50. package/src/fn-routers/task-sync-assign.test.ts +1 -2
  51. package/src/fn-routers/task-sync-assign.ts +4 -4
  52. package/src/fn-routers/task-sync-bind.test.ts +18 -18
  53. package/src/fn-routers/task-sync-brief.d.ts +96 -0
  54. package/src/fn-routers/task-sync-brief.test.ts +17 -20
  55. package/src/fn-routers/task-sync-brief.ts +14 -15
  56. package/src/fn-routers/task-sync-dispatch.d.ts +61 -0
  57. package/src/fn-routers/task-sync-dispatch.test.ts +33 -51
  58. package/src/fn-routers/task-sync-dispatch.ts +124 -0
  59. package/src/fn-routers/task-sync-lane.d.ts +49 -0
  60. package/src/fn-routers/task-sync-lane.ts +125 -0
  61. package/src/fn-routers/task-sync-projection.d.ts +121 -0
  62. package/src/fn-routers/task-sync-projection.test.ts +0 -26
  63. package/src/fn-routers/task-sync-projection.ts +0 -51
  64. package/src/fn-routers/task-sync-router.d.ts +377 -0
  65. package/src/fn-routers/task-sync-router.test.ts +2 -14
  66. package/src/fn-routers/task-sync-router.ts +1075 -0
  67. package/src/fn-routers/tasks-router.d.ts +25 -0
  68. package/src/fn-routers/user-preferences-router.d.ts +77 -0
  69. package/src/fn-routers/work-items-router.d.ts +102 -0
  70. package/src/gate-ttls.d.ts +6 -0
  71. package/src/gpt-gate.d.ts +25 -0
  72. package/src/handlers/bun-stable-watch.d.ts +1 -0
  73. package/src/handlers/pane-outbox-drain.d.ts +1 -0
  74. package/src/handlers/projects-digest.d.ts +26 -0
  75. package/src/handlers/prune-stale-data.d.ts +30 -0
  76. package/src/handlers/registry-stale-inactive-sweep.d.ts +1 -0
  77. package/src/handlers/scala-tools-currency-watch.d.ts +1 -0
  78. package/src/handlers/singleton-tools-currency-watch.d.ts +1 -0
  79. package/src/handlers/singleton-tools-currency-watch.ts +11 -114
  80. package/src/handlers/sweep-event-log.d.ts +1 -0
  81. package/src/handlers/sweep-host-cache.d.ts +1 -0
  82. package/src/handlers/sweep-host-cache.ts +26 -143
  83. package/src/handlers/upstream-issue-sync.d.ts +1 -0
  84. package/src/handlers/whatsapp-liveness-logic.d.ts +34 -0
  85. package/src/handlers/whatsapp-liveness.d.ts +4 -0
  86. package/src/hono-error-handler.d.ts +25 -0
  87. package/src/install-harness.d.ts +45 -0
  88. package/src/install-logging.d.ts +34 -0
  89. package/src/install-logging.ts +0 -3
  90. package/src/load-service-install-row.d.ts +24 -0
  91. package/src/load-service-prisma-client.d.ts +18 -0
  92. package/src/mount-orpc-router.d.ts +15 -0
  93. package/src/mount-orpc-router.ts +1 -1
  94. package/src/mount-server-packages.d.ts +29 -0
  95. package/src/mount.d.ts +9 -0
  96. package/src/mutable-app-ref.d.ts +44 -0
  97. package/src/page-route-shell-types.d.ts +12 -0
  98. package/src/page-server-orpc/audit-revert-router.d.ts +37 -0
  99. package/src/page-server-orpc/hub-mcp-client.d.ts +12 -0
  100. package/src/page-server-orpc/portal-diagnostics-router.d.ts +49 -0
  101. package/src/page-server-orpc/puck-pages-router.d.ts +58 -0
  102. package/src/page-server-orpc/server-client.d.ts +16 -0
  103. package/src/page-server-orpc/templates-router.d.ts +34 -0
  104. package/src/page-server-orpc/user-preferences-router.d.ts +79 -0
  105. package/src/pg-boss-handler-registry.d.ts +27 -0
  106. package/src/pg-boss-mount.d.ts +42 -0
  107. package/src/proposal-authorized-emails.d.ts +28 -0
  108. package/src/proposal-chat-inject.d.ts +17 -0
  109. package/src/proposal-gate.d.ts +18 -0
  110. package/src/read-html-template.d.ts +38 -0
  111. package/src/read-viewport-from-request.d.ts +18 -0
  112. package/src/readiness-gate.d.ts +16 -0
  113. package/src/register-api-routes.d.ts +18 -0
  114. package/src/register-crash-handlers.d.ts +15 -0
  115. package/src/register-page-routes.d.ts +27 -0
  116. package/src/register-page-routes.ts +3 -0
  117. package/src/register-page-server-client.d.ts +11 -0
  118. package/src/register-registry-page-route/ai-chat-prefetch.d.ts +13 -0
  119. package/src/register-registry-page-route/graph-cache.d.ts +30 -0
  120. package/src/register-registry-page-route/meta-title.d.ts +2 -0
  121. package/src/register-registry-page-route/proposal-approval-email-token.d.ts +14 -0
  122. package/src/register-registry-page-route/proposal-brand-stamping.d.ts +28 -0
  123. package/src/register-registry-page-route/resolve-page-data.d.ts +1 -0
  124. package/src/register-registry-page-route.d.ts +27 -0
  125. package/src/register-static-assets.d.ts +13 -0
  126. package/src/register-ws-routes.d.ts +28 -0
  127. package/src/resolve-app-base-url.d.ts +13 -0
  128. package/src/resolve-brand-favicon.d.ts +28 -0
  129. package/src/resolve-brand-favicon.ts +1 -1
  130. package/src/resolve-from-loader-config.d.ts +32 -0
  131. package/src/resolve-service-context.d.ts +17 -0
  132. package/src/resolve-service-port.d.ts +75 -0
  133. package/src/resolve-theme-mode.d.ts +42 -0
  134. package/src/resolvers/fetch-org-brand.d.ts +2 -0
  135. package/src/resolvers/portal-route-paths.d.ts +39 -0
  136. package/src/resolvers/website-by-host-map.d.ts +12 -0
  137. package/src/resource-chat-inject.d.ts +46 -0
  138. package/src/resource-not-found.d.ts +60 -0
  139. package/src/resource-not-found.ts +0 -10
  140. package/src/run-boot-hook-list.d.ts +19 -0
  141. package/src/run-boot-hooks.d.ts +31 -0
  142. package/src/scala-tools-currency-watchdog.d.ts +10 -0
  143. package/src/security-headers.d.ts +12 -0
  144. package/src/serve/serve.d.ts +46 -0
  145. package/src/serve/stamp-transport-identity.d.ts +32 -0
  146. package/src/serve/stamp-transport-identity.ts +2 -14
  147. package/src/server-error-page.d.ts +29 -0
  148. package/src/start-scheduled-jobs.d.ts +18 -0
  149. package/src/two-phase-boot.d.ts +136 -0
  150. package/src/two-phase-boot.ts +17 -85
  151. package/src/two-phase-health-gate.test.ts +2 -13
  152. package/src/types.d.ts +46 -0
  153. package/src/ui-service.d.ts +46 -0
  154. package/src/ws-boot-self-test.d.ts +19 -0
  155. package/src/auth-gate.test.ts +0 -70
  156. package/src/fn-routers/task-sync-close.test.ts +0 -44
  157. package/src/fn-routers/task-sync-close.ts +0 -82
  158. package/src/fn-routers/task-sync-hook-lifecycle.test.ts +0 -204
  159. package/src/fn-routers/task-sync-lane.test.ts +0 -36
  160. package/src/fn-routers/task-sync-rename.test.ts +0 -53
  161. package/src/fn-routers/task-sync-rename.ts +0 -111
  162. package/src/handlers/sweep-host-cache.test.ts +0 -133
  163. package/src/serve/stamp-transport-identity.test.ts +0 -24
  164. package/src/two-phase-health-gate-client-only.test.ts +0 -77
@@ -12,7 +12,6 @@
12
12
  import { describe, expect, it } from "bun:test";
13
13
  import {
14
14
  STATUS_TO_WORK_ITEM,
15
- deriveEvidenceProgress,
16
15
  normalizeStatus,
17
16
  planReconcile,
18
17
  resolveProjectedStatus,
@@ -30,31 +29,6 @@ describe("STATUS_TO_WORK_ITEM", () => {
30
29
  });
31
30
  });
32
31
 
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
-
58
32
  describe("status translation is a single non-drifting source", () => {
59
33
  // The forward map (CLI → work_items) is canonical; the reverse is DERIVED from
60
34
  // it (+ a closed synonym set). This locks the structural fix: the two
@@ -74,57 +74,6 @@ 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
-
128
77
  /**
129
78
  * Decide the status to WRITE for an existing row, given what the CLI list says.
130
79
  *
@@ -0,0 +1,377 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ * @indivisible-unit
6
+ *
7
+ * CLI-AGNOSTIC task-sync CORE — the bridge between a lane's native task list
8
+ * (Claude Code TodoWrite, OpenCode todowrite, future CLIs) and the `work_items`
9
+ * table, which is the single source of truth for "what work does this lane
10
+ * have." Detailed contract + the per-CLI adapter split: apps/agent-fleet.md
11
+ * (task-sync section).
12
+ *
13
+ * Design (operator requirement): work_items is the source; each CLI's task list
14
+ * is a WRITE-THROUGH PROJECTION, never a peer. One source + write-through
15
+ * projection cannot diverge by construction — no two-way sync, no conflict
16
+ * resolution. The lane↔task binding is SYMMETRIC: a lane is a lane whether the
17
+ * pane is an orchestrator or a supervised agent (both have tasks).
18
+ *
19
+ * Mapping: an operator request = a kind='task' work_item; the lane's todo items
20
+ * = kind='subtask' rows under it via parent_id. The task carries the lane
21
+ * identity in kind_data.lane = { pane, session, cli_kind, role } so any surface
22
+ * (fleet UI, another lane) can resolve "who owns this work" and push changes
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).
26
+ *
27
+ * These procedures are the ONLY CLI-agnostic surface an adapter calls:
28
+ * bindLane — bind (create-or-confirm) a lane's task. SessionStart.
29
+ * upsertFromTaskList — write a lane's current task list through to subtasks (AGENT→DB leg, OpenCode whole-list).
30
+ * upsertFromTaskHook — apply a per-item Claude Code PostToolUse:TaskCreate/Update/Delete (AGENT→DB leg, Claude per-item).
31
+ * readTaskList — read a lane's current subtasks as a normalized list (DB→AGENT backstop).
32
+ * applyExternalChange — record an out-of-band subtask change + notify the owning lane (DB→AGENT realtime leg).
33
+ * A CLI with no native task surface (Codex today — upstream_issue openai-codex-2966)
34
+ * has NO adapter: it degrades to work_items-only. The work is still tracked and
35
+ * visible in the projects UI; the lane simply has no live projection. That is a
36
+ * legitimate end state, not a gap to paper over with a fake adapter.
37
+ */
38
+ import { type Context } from "./auth-router.ts";
39
+ import * as v from "valibot";
40
+ /** A lane's native task-list item, normalized. Every CLI adapter maps to this. */
41
+ export interface TaskListItem {
42
+ content: string;
43
+ status: "pending" | "in_progress" | "completed";
44
+ /** CLI-native stable id when the CLI provides one (TodoWrite has none; opencode todo row id). */
45
+ mark?: string;
46
+ }
47
+ /**
48
+ * Input contract for upsertFromTaskList. oRPC does NOT parse the JSON body into
49
+ * `input` for a schema-less procedure (see the bindLane comment in this file) —
50
+ * so without an explicit `.input()` schema, `input.lane_session` is ALWAYS
51
+ * undefined and the procedure throws "requires lane_session" unconditionally.
52
+ * Measured 2026-07-29: every pi/opencode whole-list `task_list` publish 400'd
53
+ * for exactly this reason (the per-item upsertFromTaskHook path worked because
54
+ * it unwraps `raw.json ?? raw`). Declaring the schema is the documented remedy
55
+ * and the pattern every other `authed.input(...)` procedure in service-boot
56
+ * already follows. The pi adapter (scala-hooks.ts syncTaskList) already sends
57
+ * `{ lane_session, task_list, source }` — it was always valid; the server never
58
+ * read it.
59
+ */
60
+ export declare const upsertFromTaskListInput: v.ObjectSchema<{
61
+ readonly lane_session: v.StringSchema<undefined>;
62
+ readonly task_list: v.ArraySchema<v.ObjectSchema<{
63
+ readonly content: v.StringSchema<undefined>;
64
+ readonly status: v.PicklistSchema<["pending", "in_progress", "completed"], undefined>;
65
+ readonly mark: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
66
+ }, undefined>, undefined>;
67
+ readonly source: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
68
+ }, undefined>;
69
+ /** Who a lane IS — symmetric across orchestrator/agent roles and across CLIs. */
70
+ export interface LaneIdentity {
71
+ /** "tmux:%42" — authoritative pane identity ($TMUX_PANE). */
72
+ pane: string;
73
+ session?: string;
74
+ /** pane-inventory inferCliKind: claude-code | opencode | codex | unknown. */
75
+ cli_kind?: string;
76
+ /** agent | orchestrator — both have tasks; the model does not assume a supervised agent. */
77
+ role?: string;
78
+ /**
79
+ * Optional. work_items.organisation_id is NOT NULL; when the caller omits it
80
+ * (auto-bind at SessionStart — an adopted or channel-dispatched lane has no
81
+ * client org), bindLane defaults to the platform org via resolveBindOrg.
82
+ */
83
+ organisation_id?: string;
84
+ }
85
+ export interface WorkItem {
86
+ id: string;
87
+ kind: string;
88
+ parent_id: string | null;
89
+ organisation_id: string;
90
+ title: string;
91
+ status: string | null;
92
+ position: string | null;
93
+ completed: boolean | null;
94
+ kind_data: Record<string, unknown>;
95
+ }
96
+ export type Prisma = {
97
+ work_items: {
98
+ findFirst: (a: unknown) => Promise<WorkItem | null>;
99
+ findMany: (a: unknown) => Promise<WorkItem[]>;
100
+ create: (a: unknown) => Promise<WorkItem>;
101
+ update: (a: unknown) => Promise<WorkItem>;
102
+ deleteMany: (a: unknown) => Promise<{
103
+ count: number;
104
+ }>;
105
+ };
106
+ comments?: {
107
+ findFirst: (a: unknown) => Promise<CommentRow | null>;
108
+ };
109
+ };
110
+ /** A `comments` row — only the fields readAssignedBrief touches. */
111
+ export interface CommentRow {
112
+ content: string;
113
+ created_at: Date;
114
+ }
115
+ /**
116
+ * A dispatched run already points at its fleet_tasks row. Task-sync may fill
117
+ * the convenience join for an adopted lane, but must never replace that
118
+ * dispatch identity with the lane's work_items projection id.
119
+ */
120
+ export declare function untrackedRunForPane(pane: string): {
121
+ pane: string;
122
+ state: {
123
+ not: string;
124
+ };
125
+ tracked_task_id: null;
126
+ };
127
+ /**
128
+ * Resolve the org a lane's task is created under. Explicit when the lane knows
129
+ * its org (a dispatched fleet lane carries one); defaults to the platform org
130
+ * otherwise — an adopted or channel-dispatched pane has no client org, and the
131
+ * platform org is the home for fleet/internal lane tasks (where the operator's
132
+ * PM view reads), so auto-bind at SessionStart seeds the task there.
133
+ */
134
+ export declare function resolveBindOrg(explicit?: string): string;
135
+ /**
136
+ * Record a task-sync hook DISCARD to event_log so it is observable + queryable,
137
+ * not silently swallowed by the hook command's `>/dev/null 2>&1 || true`. The
138
+ * endpoint records it server-side regardless of what the hook does with stdout.
139
+ * Fire-and-forget per event-log-is-the-only-durable-event-log. The headline
140
+ * discard is "no-task-bound" (lane has no bound parent task — exactly the gap
141
+ * that SessionStart auto-bind closes; once binding is automatic this becomes a
142
+ * rare safety signal rather than the default outcome).
143
+ */
144
+ export declare function recordTaskSyncDiscard(reason: string, pane: string | null, extra?: Record<string, unknown>): void;
145
+ /**
146
+ * Record a discard AND return the soft {applied:false} body in one step — every
147
+ * no-op path in upsertFromTaskHook goes through here so none can silently drop
148
+ * an event. The returned shape matches the prior `{applied:false, reason,...}`.
149
+ */
150
+ export declare function discardApplied(reason: string, pane: string | null, extra?: Record<string, unknown>): {
151
+ applied: false;
152
+ reason: string;
153
+ } & Record<string, unknown>;
154
+ /**
155
+ * Trim a task subject at the first tool-call marker.
156
+ *
157
+ * A subject arriving as `Cut judge false positives</subject>\n<parameter
158
+ * name="description">Live gate blocks ~half...` is not a title with odd
159
+ * punctuation — it is the raw tool call, and it renders on the dashboard as a
160
+ * wall of XML where the operator expects a task name (observed live 2026-07-25,
161
+ * 3 rows). The malformation happens upstream in the CLI's hook payload, which
162
+ * this service does not control, so the defence belongs at the WRITE boundary:
163
+ * `work_items` is the single source of truth for every surface, and once a
164
+ * corrupted title lands there every reader shows it.
165
+ *
166
+ * Truncating rather than rejecting is deliberate: the text BEFORE the marker is
167
+ * the real subject and is worth keeping. Dropping the row instead would lose a
168
+ * task the lane genuinely created.
169
+ */
170
+ export declare function sanitizeTaskSubject(raw: string): string;
171
+ export declare function classifyTaskHookEvent(evt: {
172
+ hook_event_name?: unknown;
173
+ task_subject?: unknown;
174
+ }): "completed" | "created" | "tool";
175
+ export declare const taskSyncRouter: {
176
+ /**
177
+ * Bind (create-or-confirm) the task a lane owns. Called from a SessionStart
178
+ * adapter so the lane has a durable task row before its first todo write.
179
+ * Idempotent: re-binding updates the identity (role/cli_kind can change) but
180
+ * never orphans existing subtasks. After binding, links the lane's
181
+ * fleet_agent_runs row (matched by pane) to the task via tracked_task_id —
182
+ * the join the operator's lane-labels monitor + the unified fleet view read.
183
+ */
184
+ bindLane: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
185
+ headers: Headers;
186
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
187
+ prisma: any;
188
+ user: import("./auth-router.ts").User;
189
+ orgId: string | null | undefined;
190
+ }>, v.OptionalSchema<v.ObjectSchema<{
191
+ readonly lane: v.OptionalSchema<v.ObjectSchema<{
192
+ readonly pane: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
193
+ readonly session: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
194
+ readonly cli_kind: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
195
+ readonly role: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
196
+ readonly organisation_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
197
+ }, undefined>, undefined>;
198
+ readonly title: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
199
+ readonly parent_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
200
+ }, undefined>, undefined>, import("@orpc/contract").Schema<{
201
+ task_id: string;
202
+ created: boolean;
203
+ }, {
204
+ task_id: string;
205
+ created: boolean;
206
+ }>, Record<never, never>, Record<never, never>>;
207
+ /**
208
+ * AGENT→DB leg. Write a lane's CURRENT task list through to subtasks. The
209
+ * list is authoritative for this lane (it's what the CLI just rendered), so
210
+ * this is a full reconcile: create/update/delete to match. Matching is by
211
+ * stable mark when the CLI provides one, else by content. Does NOT dispatch
212
+ * a change notification — the lane that authored the list already has it.
213
+ */
214
+ upsertFromTaskList: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
215
+ headers: Headers;
216
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
217
+ prisma: any;
218
+ user: import("./auth-router.ts").User;
219
+ orgId: string | null | undefined;
220
+ }>, v.ObjectSchema<{
221
+ readonly lane_session: v.StringSchema<undefined>;
222
+ readonly task_list: v.ArraySchema<v.ObjectSchema<{
223
+ readonly content: v.StringSchema<undefined>;
224
+ readonly status: v.PicklistSchema<["pending", "in_progress", "completed"], undefined>;
225
+ readonly mark: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
226
+ }, undefined>, undefined>;
227
+ readonly source: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
228
+ }, undefined>, import("@orpc/contract").Schema<{
229
+ task_id: string;
230
+ created: number;
231
+ updated: number;
232
+ deleted: number;
233
+ total: number;
234
+ }, {
235
+ task_id: string;
236
+ created: number;
237
+ updated: number;
238
+ deleted: number;
239
+ total: number;
240
+ }>, Record<never, never>, Record<never, never>>;
241
+ /**
242
+ * Claude Code PostToolUse:TaskCreate/TaskUpdate hook entry. Claude Code's
243
+ * todo tool is the per-item Task* family (TaskCreate/TaskUpdate) — TodoWrite
244
+ * was REMOVED in recent Claude Code; the hook matcher keys on
245
+ * TaskCreate/TaskUpdate, never TodoWrite. The hook command posts the RAW hook
246
+ * event (stdin) + an x-lane-pane header (tmux:$TMUX_PANE). Fired as a
247
+ * PostToolUse:TaskCreate/Update hook (NOT a "TaskCreated" event — that is
248
+ * not a real Claude Code hook event + never fired); this procedure
249
+ * applies the per-item change (TaskCreate → new subtask keyed by the harness
250
+ * task id; TaskUpdate → status/title flip on the matching subtask). Server-
251
+ * side extraction keeps the hook command a trivial jq|curl. Accepts the event
252
+ * raw (input.tool_name) OR oRPC-enveloped (input.json.tool_name). Session-
253
+ * less: fn.taskSync is page-gate-exempt; localhost pane shell authenticates
254
+ * as super-admin via resolveUser. Soft-returns on any no-op (never throws).
255
+ */
256
+ upsertFromTaskHook: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
257
+ headers: Headers;
258
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
259
+ prisma: any;
260
+ user: import("./auth-router.ts").User;
261
+ orgId: string | null | undefined;
262
+ }>, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
263
+ applied: boolean;
264
+ op: string;
265
+ taskId: string;
266
+ } | ({
267
+ applied: false;
268
+ reason: string;
269
+ } & Record<string, unknown>), {
270
+ applied: boolean;
271
+ op: string;
272
+ taskId: string;
273
+ } | ({
274
+ applied: false;
275
+ reason: string;
276
+ } & Record<string, unknown>)>, Record<never, never>, Record<never, never>>;
277
+ /**
278
+ * DB→AGENT backstop read. Returns the lane's current subtasks in the
279
+ * normalized shape every CLI adapter maps to its native list. Called by the
280
+ * SessionStart/UserPromptSubmit adapters so a pane never opens or continues
281
+ * on stale tasks, and by a lane after it receives a "your tasks changed"
282
+ * channel notification.
283
+ */
284
+ readTaskList: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
285
+ headers: Headers;
286
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
287
+ prisma: any;
288
+ user: import("./auth-router.ts").User;
289
+ orgId: string | null | undefined;
290
+ }>, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
291
+ task_id: null;
292
+ task_title: null;
293
+ subtasks: never[];
294
+ } | {
295
+ task_id: string;
296
+ task_title: string;
297
+ subtasks: TaskListItem[];
298
+ }, {
299
+ task_id: null;
300
+ task_title: null;
301
+ subtasks: never[];
302
+ } | {
303
+ task_id: string;
304
+ task_title: string;
305
+ subtasks: TaskListItem[];
306
+ }>, Record<never, never>, Record<never, never>>;
307
+ /**
308
+ * DB→AGENT self-brief leg. A spawned lane PULLS its own brief — nothing is
309
+ * pasted, typed, passed as an argument or sent over a channel
310
+ * (`action-emission-architecture.md` "The lane PULLS its brief"). The lane's
311
+ * CLI extension reads its own session id on load, calls this procedure, and
312
+ * reads the brief from the task's LATEST comment. The comment — not the
313
+ * description, not a stale comment — is the brief for THIS run: every spawn
314
+ * writes a NEW comment, so "what was this lane told" always has exactly one
315
+ * answer (`assignment-as-dispatch`). Handler lives in task-sync-brief.ts
316
+ * (router is at its line budget); mounted here so fn.taskSync surfaces it.
317
+ */
318
+ readAssignedBrief: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
319
+ headers: Headers;
320
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
321
+ prisma: any;
322
+ user: import("./auth-router.ts").User;
323
+ orgId: string | null | undefined;
324
+ }>, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
325
+ work_item_id: string | null;
326
+ brief: string | null;
327
+ comment_created_at: string | null;
328
+ status: import("./task-sync-brief.ts").BriefReadStatus;
329
+ }, {
330
+ work_item_id: string | null;
331
+ brief: string | null;
332
+ comment_created_at: string | null;
333
+ status: import("./task-sync-brief.ts").BriefReadStatus;
334
+ }>, Record<never, never>, Record<never, never>>;
335
+ /** 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. */
336
+ createBriefTask: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
337
+ headers: Headers;
338
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
339
+ prisma: any;
340
+ user: import("./auth-router.ts").User;
341
+ orgId: string | null | undefined;
342
+ }>, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
343
+ work_item_id: string;
344
+ }, {
345
+ work_item_id: string;
346
+ }>, Record<never, never>, Record<never, never>>;
347
+ /**
348
+ * DB→AGENT realtime leg (explicit dispatch path). Record an out-of-band
349
+ * change to a subtask's status (operator toggles it in the projects UI,
350
+ * another lane edits a shared task) and notify the OWNING lane's pane that
351
+ * its projection is stale. The pane's adapter re-reads (readTaskList) and
352
+ * updates its native list. Symmetric counterpart to upsertFromTaskList:
353
+ * there the lane is the originator (no self-echo); here the lane is the
354
+ * recipient.
355
+ *
356
+ * COMPLETION STEP (not MVP): this procedure is the explicit trigger. For
357
+ * FULL coverage — every work_items subtask write, including the projects
358
+ * UI's generated CRUD — a Postgres trigger + LISTEN subscriber on
359
+ * scala-channel fires the same publish, so an operator edit reaches the
360
+ * lane with no explicit call. Tracked in apps/agent-fleet.md task-sync.
361
+ */
362
+ applyExternalChange: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<Context & Record<never, never>, Context, Context>, import("@orpc/server").MergedCurrentContext<Context, {
363
+ headers: Headers;
364
+ resolveUser?: (headers: Headers, prisma: any) => Promise<import("./auth-router.ts").User | null>;
365
+ prisma: any;
366
+ user: import("./auth-router.ts").User;
367
+ orgId: string | null | undefined;
368
+ }>, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
369
+ task_id: string;
370
+ lane_pane: string | null;
371
+ notified: boolean;
372
+ }, {
373
+ task_id: string;
374
+ lane_pane: string | null;
375
+ notified: boolean;
376
+ }>, Record<never, never>, Record<never, never>>;
377
+ };
@@ -337,20 +337,8 @@ 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
- [
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" }],
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,
354
342
  "claude-code",
355
343
  );
356
344
  const update = plan.toUpdate.find((u) => u.id === "row-1");