@huynle/hub 0.7.4 → 0.8.1

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 (241) hide show
  1. package/CHANGELOG.md +430 -1
  2. package/Dockerfile +7 -2
  3. package/README.md +230 -32
  4. package/package.json +5 -3
  5. package/packages/claude-code/package.json +1 -1
  6. package/packages/claude-code/src/daemon.ts +8 -1
  7. package/packages/claude-code/src/launcher.ts +29 -9
  8. package/packages/claude-code/src/paths.ts +3 -3
  9. package/packages/claude-code/src/profile.ts +173 -0
  10. package/packages/claude-code/src/sessions.ts +22 -4
  11. package/packages/cli/dist/hub-cli.js +6871 -3460
  12. package/packages/cli/package.json +1 -1
  13. package/packages/cli/src/artifact-jobs-mcp.ts +226 -0
  14. package/packages/cli/src/artifacts-mcp.ts +160 -0
  15. package/packages/cli/src/bootstrap/adapters/node-services/render.ts +2 -2
  16. package/packages/cli/src/bootstrap/discovery/host.ts +15 -1
  17. package/packages/cli/src/bootstrap/discovery/probe.ts +1 -1
  18. package/packages/cli/src/bootstrap/enrollment/enroll.ts +21 -9
  19. package/packages/cli/src/bootstrap/enrollment/policy.ts +138 -12
  20. package/packages/cli/src/bootstrap/enrollment/status.ts +3 -2
  21. package/packages/cli/src/cli.ts +96 -14
  22. package/packages/cli/src/format.ts +48 -10
  23. package/packages/cli/src/http.ts +2 -0
  24. package/packages/cli/src/intents.ts +23 -0
  25. package/packages/cli/src/launches.ts +9 -3
  26. package/packages/cli/src/login.ts +20 -6
  27. package/packages/cli/src/mcp.ts +385 -10
  28. package/packages/cli/src/messaging.ts +7 -3
  29. package/packages/cli/src/profiles-mcp.ts +301 -0
  30. package/packages/cli/src/setup/commands.ts +23 -4
  31. package/packages/cli/src/setup/runtime.ts +3 -2
  32. package/packages/cli/src/spoke-services.ts +1 -1
  33. package/packages/cli/src/team-intake-mcp.ts +158 -0
  34. package/packages/cli/src/teams-mcp.ts +460 -0
  35. package/packages/cli/src/watchlist.ts +135 -0
  36. package/packages/codex/package.json +1 -1
  37. package/packages/codex/src/paths.ts +3 -3
  38. package/packages/codex/src/profile.ts +163 -0
  39. package/packages/codex/src/sessions.ts +12 -0
  40. package/packages/codex/src/spoke.ts +19 -1
  41. package/packages/node/package.json +1 -1
  42. package/packages/node/src/config.ts +104 -2
  43. package/packages/node/src/opencode-warm.ts +344 -0
  44. package/packages/node/src/run.ts +28 -1
  45. package/packages/preset/package.json +1 -1
  46. package/packages/preset/src/profile.ts +34 -0
  47. package/packages/preset/src/spoke.ts +4 -0
  48. package/packages/protocol/package.json +1 -1
  49. package/packages/protocol/src/artifacts.ts +142 -0
  50. package/packages/protocol/src/concierge.ts +366 -0
  51. package/packages/protocol/src/contracts.ts +104 -0
  52. package/packages/protocol/src/graph.ts +70 -0
  53. package/packages/protocol/src/hub-workspace.ts +201 -0
  54. package/packages/protocol/src/index.ts +175 -6
  55. package/packages/protocol/src/launch.ts +36 -0
  56. package/packages/protocol/src/profiles.ts +301 -0
  57. package/packages/protocol/src/teams.ts +204 -0
  58. package/packages/runtime-openshell/package.json +1 -1
  59. package/packages/runtime-sdk/package.json +1 -1
  60. package/packages/sdk/package.json +1 -1
  61. package/packages/sdk/src/address.ts +50 -2
  62. package/packages/sdk/src/artifact-jobs.ts +194 -0
  63. package/packages/sdk/src/artifacts.ts +187 -0
  64. package/packages/sdk/src/client.ts +226 -5
  65. package/packages/sdk/src/concierge.ts +88 -0
  66. package/packages/sdk/src/errors.ts +4 -1
  67. package/packages/sdk/src/index.ts +6 -0
  68. package/packages/sdk/src/team-intake.ts +122 -0
  69. package/packages/sdk/src/teams.ts +346 -0
  70. package/packages/sdk/src/types.ts +568 -1
  71. package/packages/sdk/src/wake.ts +20 -0
  72. package/packages/spoke-sdk/package.json +1 -1
  73. package/packages/spoke-sdk/src/index.ts +1 -0
  74. package/packages/spoke-sdk/src/profile-apply.ts +245 -0
  75. package/src/central/actions.ts +7 -0
  76. package/src/central/agent/acting-routes.ts +2 -1
  77. package/src/central/agent/acting-switch.ts +2 -1
  78. package/src/central/agent/acting.ts +218 -10
  79. package/src/central/agent/app-tokens.ts +3 -2
  80. package/src/central/agent/audit.ts +18 -1
  81. package/src/central/agent/body.ts +163 -0
  82. package/src/central/agent/decisions.ts +125 -2
  83. package/src/central/agent/delivery.ts +194 -40
  84. package/src/central/agent/directory-routes.ts +37 -7
  85. package/src/central/agent/directory.ts +70 -14
  86. package/src/central/agent/errors.ts +7 -0
  87. package/src/central/agent/escalation.ts +10 -2
  88. package/src/central/agent/events-routes.ts +21 -4
  89. package/src/central/agent/events.ts +18 -4
  90. package/src/central/agent/form-check.ts +85 -0
  91. package/src/central/agent/grant-requests.ts +2 -1
  92. package/src/central/agent/grant-routes.ts +4 -3
  93. package/src/central/agent/grants.ts +29 -2
  94. package/src/central/agent/idempotency.ts +133 -0
  95. package/src/central/agent/intent-routes.ts +116 -0
  96. package/src/central/agent/intents.ts +505 -0
  97. package/src/central/agent/interaction-graph-routes.ts +212 -0
  98. package/src/central/agent/interaction-graph.ts +865 -0
  99. package/src/central/agent/launch-engine.ts +189 -32
  100. package/src/central/agent/launch-routes.ts +26 -10
  101. package/src/central/agent/launch-spec.ts +40 -3
  102. package/src/central/agent/launch-store.ts +55 -2
  103. package/src/central/agent/message-limits.ts +89 -11
  104. package/src/central/agent/message-routes.ts +10 -4
  105. package/src/central/agent/messages.ts +28 -2
  106. package/src/central/agent/names.ts +12 -2
  107. package/src/central/agent/placement.ts +93 -18
  108. package/src/central/agent/read-routes.ts +89 -19
  109. package/src/central/agent/routes.ts +178 -5
  110. package/src/central/agent/webhook-routes.ts +2 -1
  111. package/src/central/agent/webhooks.ts +17 -2
  112. package/src/central/agent-data.ts +4 -0
  113. package/src/central/artifacts/access.ts +44 -0
  114. package/src/central/artifacts/blob-store.ts +113 -0
  115. package/src/central/artifacts/blob-support.ts +98 -0
  116. package/src/central/artifacts/events.ts +27 -0
  117. package/src/central/artifacts/host-caps.ts +162 -0
  118. package/src/central/artifacts/host.ts +315 -0
  119. package/src/central/artifacts/idempotency.ts +109 -0
  120. package/src/central/artifacts/jobs-routes.ts +776 -0
  121. package/src/central/artifacts/jobs.ts +344 -0
  122. package/src/central/artifacts/maintenance.ts +103 -0
  123. package/src/central/artifacts/routes.ts +360 -0
  124. package/src/central/artifacts/store.ts +776 -0
  125. package/src/central/artifacts/workspaces.ts +31 -0
  126. package/src/central/backend/file-blobs.ts +377 -0
  127. package/src/central/backend/index.ts +12 -3
  128. package/src/central/backend/memory-blobs.ts +116 -0
  129. package/src/central/backend/postgres-blobs.ts +513 -0
  130. package/src/central/backend/postgres-migrate.ts +33 -0
  131. package/src/central/backend/postgres-state.ts +19 -0
  132. package/src/central/backend/types.ts +8 -0
  133. package/src/central/concierge/activity.ts +142 -0
  134. package/src/central/concierge/arm.ts +63 -0
  135. package/src/central/concierge/board-render.ts +190 -0
  136. package/src/central/concierge/board-results.ts +169 -0
  137. package/src/central/concierge/board-routes.ts +350 -0
  138. package/src/central/concierge/board.ts +249 -0
  139. package/src/central/concierge/context.ts +221 -0
  140. package/src/central/concierge/entitlements.ts +13 -0
  141. package/src/central/concierge/guard.ts +120 -0
  142. package/src/central/concierge/inbound.ts +86 -0
  143. package/src/central/concierge/index.ts +84 -0
  144. package/src/central/concierge/notes.ts +41 -0
  145. package/src/central/concierge/notifier.ts +986 -0
  146. package/src/central/concierge/prompt.ts +131 -0
  147. package/src/central/concierge/proposals.ts +268 -0
  148. package/src/central/concierge/push-remap.ts +49 -0
  149. package/src/central/concierge/routes.ts +122 -0
  150. package/src/central/concierge/schedule.ts +113 -0
  151. package/src/central/concierge/service.ts +243 -0
  152. package/src/central/concierge/session-routes.ts +125 -0
  153. package/src/central/concierge/settings.ts +217 -0
  154. package/src/central/concierge/supervisor.ts +767 -0
  155. package/src/central/concierge/tiers.ts +260 -0
  156. package/src/central/concierge/watchlist-routes.ts +279 -0
  157. package/src/central/concierge/watchlist.ts +273 -0
  158. package/src/central/config.ts +28 -6
  159. package/src/central/http.ts +660 -54
  160. package/src/central/main.ts +1 -1
  161. package/src/central/mcp-http.ts +10 -21
  162. package/src/central/profiles/apply.ts +333 -0
  163. package/src/central/profiles/content.ts +174 -0
  164. package/src/central/profiles/events.ts +40 -0
  165. package/src/central/profiles/idempotency.ts +14 -0
  166. package/src/central/profiles/import.ts +139 -0
  167. package/src/central/profiles/routes.ts +356 -0
  168. package/src/central/profiles/store.ts +597 -0
  169. package/src/central/profiles/usage-ports.ts +120 -0
  170. package/src/central/profiles/usage.ts +364 -0
  171. package/src/central/push.ts +7 -0
  172. package/src/central/registry.ts +69 -7
  173. package/src/central/store.ts +4 -0
  174. package/src/central/teams/collections.ts +34 -0
  175. package/src/central/teams/discovery.ts +346 -0
  176. package/src/central/teams/draft-edits.ts +46 -0
  177. package/src/central/teams/events.ts +46 -0
  178. package/src/central/teams/intake-routes.ts +100 -0
  179. package/src/central/teams/intake.ts +786 -0
  180. package/src/central/teams/policy.ts +231 -0
  181. package/src/central/teams/routes.ts +516 -0
  182. package/src/central/teams/seats.ts +272 -0
  183. package/src/central/teams/staffing.ts +910 -0
  184. package/src/central/teams/store.ts +667 -0
  185. package/src/central/teams/validation.ts +256 -0
  186. package/src/central/workspaces/board.ts +408 -0
  187. package/src/central/workspaces/checkpoints.ts +316 -0
  188. package/src/central/workspaces/coordinator.ts +841 -0
  189. package/src/central/workspaces/events.ts +85 -0
  190. package/src/central/workspaces/lifecycle.ts +93 -0
  191. package/src/central/workspaces/master.ts +490 -0
  192. package/src/central/workspaces/members.ts +632 -0
  193. package/src/central/workspaces/routes.ts +354 -0
  194. package/src/central/workspaces/sidechats-routes.ts +410 -0
  195. package/src/central/workspaces/sidechats.ts +482 -0
  196. package/src/central/workspaces/store.ts +571 -0
  197. package/src/central/workspaces/supervision.ts +355 -0
  198. package/src/index.ts +126 -23
  199. package/src/migrate.ts +109 -3
  200. package/src/spoke/concierge-agent.ts +33 -0
  201. package/src/spoke/deliver.ts +22 -4
  202. package/src/spoke/env-inject.ts +8 -1
  203. package/src/spoke/fork-revert.ts +142 -0
  204. package/src/spoke/hub-rpc.ts +41 -5
  205. package/src/spoke/hub-tool.ts +404 -25
  206. package/src/spoke/launchers.ts +6 -4
  207. package/src/spoke/listing.ts +65 -10
  208. package/src/spoke/localhub.ts +14 -4
  209. package/src/spoke/locations.ts +40 -0
  210. package/src/spoke/profile-apply.ts +298 -0
  211. package/src/spoke/session-context.ts +256 -0
  212. package/src/spoke/spoke.ts +330 -23
  213. package/src/spoke/timeline.ts +6 -1
  214. package/src/spoke/tracker.ts +23 -1
  215. package/src/spoke/who-teams.ts +81 -0
  216. package/src/tui-messaging.ts +32 -18
  217. package/web/dist/assets/ArtifactPage-mNzjlP_U.js +1 -0
  218. package/web/dist/assets/ArtifactPane-CPQcuX7c.js +1 -0
  219. package/web/dist/assets/ArtifactWorkbench-9eTj1CeW.css +1 -0
  220. package/web/dist/assets/ArtifactWorkbench-NoFC5Sb2.js +6 -0
  221. package/web/dist/assets/Library-DYsFj_5c.css +1 -0
  222. package/web/dist/assets/Library-LXHsP5Fw.js +2 -0
  223. package/web/dist/assets/ProfilesDestination-DhVVcAep.js +1 -0
  224. package/web/dist/assets/TeamsPage-DBrO5hoL.js +1 -0
  225. package/web/dist/assets/Watchtower-CTgqHk5g.css +1 -0
  226. package/web/dist/assets/Watchtower-DxaJYQp_.js +2 -0
  227. package/web/dist/assets/index-BabrNDB3.css +1 -0
  228. package/web/dist/assets/index-lIOfSYck.js +34 -0
  229. package/web/dist/assets/live-BDq0AABg.js +5 -0
  230. package/web/dist/assets/live-BUofsQpz.css +1 -0
  231. package/web/dist/assets/pane-layout-DWXfDU-1.js +1 -0
  232. package/web/dist/assets/plural-Bspvoqfq.js +1 -0
  233. package/web/dist/assets/renderer2d-DAr4IHM8.js +1 -0
  234. package/web/dist/assets/renderer3d-DWZj595u.js +5393 -0
  235. package/web/dist/assets/safe-area-JUb6OeI3.js +1 -0
  236. package/web/dist/assets/source-CDU04kYw.js +1 -0
  237. package/web/dist/assets/ui-Dh1-78Cl.js +3 -0
  238. package/web/dist/index.html +5 -2
  239. package/web/dist/sw.js +22 -2
  240. package/web/dist/assets/index-Dm8OmkaO.css +0 -1
  241. package/web/dist/assets/index-FUJbj_Du.js +0 -20
@@ -50,6 +50,33 @@ export interface SessionView {
50
50
  /** In sessions answers. */
51
51
  readonly machineID?: string
52
52
  readonly project?: string
53
+ /** (CON-02) The stored intent, when there is one (untrusted text). */
54
+ readonly intent?: IntentField
55
+ }
56
+
57
+ /** (CON-02) Who set a session's intent, highest precedence first. */
58
+ export type IntentSource = "user" | "launch" | "self" | "prompt" | "title"
59
+
60
+ /**
61
+ * (CON-02) What a session is for (GET/PUT /api/agent/sessions/:addr/intent): untrusted text, sanitised, one line,
62
+ * 1–280 characters. Mirrors packages/protocol SessionIntent.
63
+ */
64
+ export interface SessionIntent {
65
+ readonly sessionID: string
66
+ readonly text: string
67
+ readonly source: IntentSource
68
+ /** 0 before the hub stored anything (a title fallback). */
69
+ readonly revision: number
70
+ readonly updatedAt: number
71
+ /** user, app:<tokenID> or ses_… */
72
+ readonly by?: string
73
+ }
74
+
75
+ /** (CON-02) The intent as list and show entries embed it. */
76
+ export interface IntentField {
77
+ readonly text: string
78
+ readonly source: IntentSource
79
+ readonly revision: number
53
80
  }
54
81
 
55
82
  export interface AttentionItem {
@@ -71,6 +98,8 @@ export interface Status {
71
98
  }[]
72
99
  readonly attention: readonly AttentionItem[]
73
100
  readonly you?: Actor
101
+ /** HWA-40: the listed sessions' teams (a team member: its own teams only). */
102
+ readonly teams?: readonly TeamGroupView[]
74
103
  }
75
104
 
76
105
  /** GET /api/agent/sessions/:addr: one session in detail. */
@@ -86,6 +115,8 @@ export interface SessionDetail extends SessionView {
86
115
  }
87
116
  readonly unsupported?: readonly { readonly feature?: string; readonly harness?: string }[]
88
117
  readonly failed?: readonly { readonly feature?: string }[]
118
+ /** HWA-40: the session's teams as the caller may see them. */
119
+ readonly teams?: readonly TeamMembershipView[]
89
120
  }
90
121
 
91
122
  /** One timeline row (newest first). */
@@ -119,6 +150,46 @@ export interface DirectoryEntry {
119
150
  readonly lastSeen?: number
120
151
  readonly archived?: boolean
121
152
  readonly roles: readonly RoleView[]
153
+ /** (CON-02) The stored intent, when there is one (untrusted text). */
154
+ readonly intent?: IntentField
155
+ /** HWA-40 (additive): the harness when reported, team seats and the caller's reach (team members only). */
156
+ readonly harness?: string
157
+ readonly teams?: readonly TeamMembershipView[]
158
+ readonly reach?: TeamReach
159
+ }
160
+
161
+ /** HWA-40: what the caller may send to / receive from a session under team policy (from the hub's verdict). */
162
+ export interface TeamReach {
163
+ readonly send: readonly ("msg" | "ask")[]
164
+ readonly receive: readonly ("msg" | "ask")[]
165
+ readonly direction: "two-way" | "outbound" | "inbound" | "none"
166
+ readonly unreachable?: true
167
+ }
168
+
169
+ /** HWA-40: one team a session is seated in, with its roles there. */
170
+ export interface TeamMembershipView {
171
+ readonly teamID: string
172
+ readonly team: string
173
+ readonly roles: readonly string[]
174
+ readonly reach?: TeamReach
175
+ }
176
+
177
+ /** HWA-40: a team in who/status answers, with its listed members. */
178
+ export interface TeamGroupView {
179
+ readonly teamID: string
180
+ readonly name: string
181
+ readonly title: string
182
+ readonly mode: "open" | "restricted"
183
+ readonly paused?: true
184
+ readonly yourRoles?: readonly string[]
185
+ readonly members: readonly { readonly sessionID: string; readonly role: string; readonly generation: number }[]
186
+ }
187
+
188
+ /** GET /api/agent/who. `scope: "team"`: the caller sees its teams only (HWA-40). */
189
+ export interface WhoAnswer {
190
+ readonly sessions: DirectoryEntry[]
191
+ readonly teams?: readonly TeamGroupView[]
192
+ readonly scope?: "team"
122
193
  }
123
194
 
124
195
  /** One compact event of GET /api/agent/events (no free text). Unknown types are valid: ignore them. */
@@ -198,7 +269,11 @@ export type MessageKind = "msg" | "ask" | "reply" | "decline"
198
269
  /** `sent` for msg/reply/decline; asks are open until answered, declined or expired. */
199
270
  export type MessageState = "sent" | "open" | "answered" | "declined" | "expired"
200
271
  /** How a message reached its recipient session. */
201
- export type MessageDelivery = "steer" | "turn" | "queued" | "retained" | "pending"
272
+ /**
273
+ * (CON-10) "dropped": it was pending for a session that ended for good (a retired Concierge session); never delivered.
274
+ * "expired": it was pending for 24 h (its recipient stayed offline or busy); never delivered, and its sender was told.
275
+ */
276
+ export type MessageDelivery = "steer" | "turn" | "queued" | "retained" | "pending" | "dropped" | "expired"
202
277
 
203
278
  /** A stored agent message. */
204
279
  export interface AgentMessage {
@@ -224,10 +299,21 @@ export interface AgentMessage {
224
299
  readonly answer?: { readonly id: string; readonly at: number }
225
300
  }
226
301
 
302
+ /**
303
+ * Why a message to a session did not start a turn: the sender asked wake:false, the recipient is a subagent
304
+ * (never woken), the reply chain went past the hop cap (8 hops without a human turn), or the recipient's wake
305
+ * budget (30 per 10 minutes) is spent. The message is still delivered.
306
+ */
307
+ export type WakeSkip = "wake_false" | "subagent" | "hop_cap" | "budget"
308
+
227
309
  /** POST /api/agent/msg and /ask: the stored message and how it was delivered (none: stored only). */
228
310
  export interface Sent {
229
311
  readonly message: AgentMessage
230
312
  readonly delivery?: MessageDelivery
313
+ /** Messages to a session: whether the hub sent it to wake the recipient (absent from hubs before 0.8.1). */
314
+ readonly woke?: boolean
315
+ /** When woke is false: why. */
316
+ readonly wakeSkipped?: WakeSkip
231
317
  }
232
318
 
233
319
  /** POST /api/agent/msg or /ask with all: one per session. */
@@ -449,9 +535,13 @@ export interface LaunchSpec {
449
535
  readonly runtime?: { readonly kind: RuntimeKind; readonly profile?: string }
450
536
  readonly placement?: { readonly labels?: Readonly<Record<string, string>>; readonly machine?: string; readonly mode?: PlacementMode }
451
537
  readonly prompt?: string
538
+ /** (CON-02) What the session is for: one line, at most 280 characters; becomes its intent (source launch). */
539
+ readonly intent?: string
452
540
  /** Free-form; never interpreted by the hub (find launches and sessions by them). */
453
541
  readonly labels?: Readonly<Record<string, string>>
454
542
  readonly end?: { readonly when?: LaunchEndWhen; readonly timeout?: string; readonly cleanup?: CleanupPolicy }
543
+ /** Pin and apply this profile version before the prompt (explicit version; apps need profiles:assign). */
544
+ readonly agentProfile?: { readonly profileID: string; readonly version: number }
455
545
  }
456
546
 
457
547
  export interface LaunchStep {
@@ -501,6 +591,12 @@ export interface LaunchView {
501
591
  readonly updated: number
502
592
  /** Who launched it: `user`, `app:<token id>` or a session ID. */
503
593
  readonly by?: string
594
+ /** The launcher's label when it launched (an app token's label). */
595
+ readonly byLabel?: string
596
+ /** The app token that launched it was revoked: it still runs and counts toward the cap until stopped. */
597
+ readonly launcherRevoked?: boolean
598
+ /** Only while queued: why it waits (`reason` is the same text). */
599
+ readonly waiting?: { readonly wait: "capacity" | "no_match" | "active"; readonly reason: string; readonly since: number }
504
600
  readonly machineID?: string
505
601
  readonly directory?: string
506
602
  readonly sessionID?: string
@@ -515,3 +611,474 @@ export interface LaunchCreated {
515
611
  readonly launch: LaunchView
516
612
  readonly created: boolean
517
613
  }
614
+
615
+ // Profiles (HWA-14; packages/protocol/src/profiles.ts is the wire source of truth). A profile is a versioned bundle
616
+ // of instructions, skills and commands; versions are immutable, and a session pins one. Assigning confers no
617
+ // authority.
618
+
619
+ export type ProfileState = "active" | "archived"
620
+
621
+ export interface ProfileSkill {
622
+ readonly name: string
623
+ readonly description: string
624
+ readonly body: string
625
+ }
626
+
627
+ export interface ProfileCommand {
628
+ readonly name: string
629
+ readonly description: string
630
+ /** Text with {{name}} parameters and {{secret:NAME}} references (never resolved by the hub). */
631
+ readonly template: string
632
+ }
633
+
634
+ export interface ProfileContent {
635
+ readonly instructions?: string
636
+ readonly model?: string
637
+ readonly skills: readonly ProfileSkill[]
638
+ readonly commands: readonly ProfileCommand[]
639
+ }
640
+
641
+ export interface ProfileView {
642
+ readonly v: 1
643
+ readonly id: string
644
+ readonly name: string
645
+ readonly title: string
646
+ readonly description?: string
647
+ readonly state: ProfileState
648
+ readonly tags?: readonly string[]
649
+ /** (RP-08) Optional bounded key/value details (≤ 32 slug keys, values 1–200 characters). */
650
+ readonly metadata?: Readonly<Record<string, string>>
651
+ /** (RP-08) Project names the profile is meant for (≤ 16). */
652
+ readonly projects?: readonly string[]
653
+ readonly latestVersion: number
654
+ readonly revision: number
655
+ readonly updatedAt: number
656
+ }
657
+
658
+ export interface ProfileVersion {
659
+ readonly v: 1
660
+ readonly profileID: string
661
+ readonly version: number
662
+ readonly content: ProfileContent
663
+ readonly digest: string
664
+ readonly createdAt: number
665
+ }
666
+
667
+ /** A version without its bodies (load skills on demand). */
668
+ export interface ProfileVersionSummary {
669
+ readonly profileID: string
670
+ readonly version: number
671
+ readonly digest: string
672
+ readonly createdAt: number
673
+ readonly bytes: number
674
+ readonly instructions: boolean
675
+ readonly model?: string
676
+ readonly skills: readonly { readonly name: string; readonly description: string }[]
677
+ readonly commands: readonly { readonly name: string; readonly description: string; readonly params: readonly string[] }[]
678
+ readonly secretRefs: readonly string[]
679
+ }
680
+
681
+ export interface ProfileVersionEntry {
682
+ readonly version: number
683
+ readonly digest: string
684
+ readonly bytes: number
685
+ readonly createdAt: number
686
+ /** (RP-08) Content counts (absent on versions written before). */
687
+ readonly skills?: number
688
+ readonly commands?: number
689
+ readonly instructions?: boolean
690
+ }
691
+
692
+ export interface ProfileCreated {
693
+ readonly profile: ProfileView
694
+ readonly version: ProfileVersionSummary
695
+ /** Import only: what was dropped or defaulted. */
696
+ readonly notes?: readonly string[]
697
+ }
698
+
699
+ export interface ProfileAck {
700
+ /** restart_required: accepted, effective after the session restarts. */
701
+ readonly status: "pending" | "applied" | "partial" | "rejected" | "unsupported" | "restart_required"
702
+ readonly digest: string
703
+ readonly notes?: readonly string[]
704
+ readonly at: number
705
+ /** With pending: no spoke reports the session now; applied when one does. */
706
+ readonly offline?: true
707
+ }
708
+
709
+ /** The version a session last confirmed running (kept across later pins). */
710
+ export interface ProfileEffective {
711
+ readonly profileID: string
712
+ readonly version: number
713
+ readonly digest: string
714
+ readonly status: "applied" | "partial"
715
+ readonly at: number
716
+ }
717
+
718
+ export interface ProfileAssignment {
719
+ readonly v: 1
720
+ readonly sessionID: string
721
+ readonly profileID: string
722
+ readonly version: number
723
+ readonly revision: number
724
+ readonly ack?: ProfileAck
725
+ readonly effective?: ProfileEffective
726
+ }
727
+
728
+ export interface SessionProfile {
729
+ readonly assignment: ProfileAssignment | null
730
+ readonly profile?: ProfileView
731
+ readonly version?: ProfileVersionSummary
732
+ }
733
+
734
+ export interface ProfileSkillLoaded {
735
+ readonly skill: ProfileSkill
736
+ readonly profileID: string
737
+ readonly version: number
738
+ }
739
+
740
+ export interface ProfileRendered {
741
+ /** The rendered text; sending it to a session is a separate prompt (its own grants). */
742
+ readonly prompt: string
743
+ readonly secretRefs: readonly string[]
744
+ readonly profileID: string
745
+ readonly version: number
746
+ readonly command: string
747
+ }
748
+
749
+ export interface ProfilePage<T> {
750
+ readonly items: readonly T[]
751
+ readonly cursor?: string
752
+ }
753
+
754
+ // Profile usage (RP-08): GET /profiles/:id/usage and GET /profiles?include=usage. A category the caller may not read
755
+ // (app scopes: sessions:read, teams:read|teams:manage, sessions:launch, workspaces:read + sessions:read) is left out
756
+ // and named in `hidden`; lists past their cap are named in `truncated` (counts stay exact).
757
+ export type ProfileUsageCategory = "sessions" | "teams" | "launches" | "workspaces"
758
+
759
+ export interface ProfileUsageCounts {
760
+ readonly sessions?: number
761
+ readonly online?: number
762
+ /** Sessions pinned to a version older than the latest. */
763
+ readonly olderVersion?: number
764
+ readonly teamRoles?: number
765
+ /** Seat occupancy: only for callers who may also read sessions (apps: sessions:read). */
766
+ readonly staffedSeats?: number
767
+ readonly launches?: number
768
+ readonly liveLaunches?: number
769
+ readonly workspaces?: number
770
+ }
771
+
772
+ export interface ProfileUsageListCounts extends ProfileUsageCounts {
773
+ /** Team names (slugs) whose roles name the profile. */
774
+ readonly teams?: readonly string[]
775
+ readonly harnesses?: readonly string[]
776
+ }
777
+
778
+ export interface ProfileUsageSession {
779
+ readonly sessionID: string
780
+ readonly name?: string
781
+ readonly title?: string
782
+ readonly machineID?: string
783
+ readonly harness?: string
784
+ readonly project?: string
785
+ readonly online: boolean
786
+ readonly lastSeen?: number
787
+ /** false: no longer in the directory (the pin is still stored). */
788
+ readonly known?: false
789
+ /** The requested (pinned) version and the pin's revision (expectedRevision for an explicit upgrade). */
790
+ readonly version: number
791
+ readonly revision: number
792
+ readonly olderVersion: boolean
793
+ readonly ack?: { readonly status: ProfileAck["status"]; readonly at: number; readonly offline?: true }
794
+ readonly effective?: { readonly version: number; readonly status: "applied" | "partial"; readonly at: number }
795
+ }
796
+
797
+ export interface ProfileUsage {
798
+ readonly profileID: string
799
+ readonly latestVersion: number
800
+ readonly counts: ProfileUsageCounts
801
+ readonly sessions?: ProfilePage<ProfileUsageSession>
802
+ readonly projects?: readonly string[]
803
+ readonly teamRoles?: readonly { readonly teamID: string; readonly teamName: string; readonly teamTitle: string; readonly role: string; readonly label?: string; readonly version?: number; /** Only with sessions:read (users always). */ readonly staffed?: number }[]
804
+ readonly launches?: readonly { readonly id: string; readonly state: string; readonly outcome?: string; readonly version: number; readonly sessionID?: string; readonly created: number; readonly updated: number }[]
805
+ readonly workspaces?: readonly { readonly workspaceID: string; readonly title?: string; readonly sessions: number }[]
806
+ readonly hidden?: readonly ProfileUsageCategory[]
807
+ readonly truncated?: readonly ("sessions" | "teamRoles" | "launches" | "workspaces")[]
808
+ }
809
+
810
+ export type ProfileListInclude = "usage" | "summary"
811
+
812
+ /** GET /profiles?include=usage,summary: the page plus counts per listed profile. */
813
+ export interface ProfileListPage extends ProfilePage<ProfileView> {
814
+ readonly usage?: Readonly<Record<string, ProfileUsageListCounts>>
815
+ /** include=summary: the latest version's counts. */
816
+ readonly summaries?: Readonly<Record<string, { readonly version: number; readonly skills: number; readonly commands: number; readonly instructions: boolean }>>
817
+ readonly hidden?: readonly ProfileUsageCategory[]
818
+ readonly truncated?: readonly "sessions"[]
819
+ }
820
+
821
+ // Workspaces (HWA-06; packages/protocol/src/hub-workspace.ts is the wire source of truth). A workspace is a durable,
822
+ // owner-tenanted coordination record: host repo bindings, members (sessions by ID), assignments and a supervision
823
+ // graph. Membership, roles and supervision confer no authority; native parent/root session ancestry is untouched.
824
+
825
+ export type WorkspaceState = "active" | "archived"
826
+ export type WorkspaceRole = "master" | "worker" | "observer"
827
+ export type WorkspaceAssignmentStatus = "open" | "in_progress" | "blocked" | "done" | "cancelled"
828
+ /** What the hub knows about a member's session now. */
829
+ export type WorkspaceSessionPresence = "online" | "offline" | "archived" | "deleted" | "unknown"
830
+
831
+ export interface WorkspaceView {
832
+ readonly v: 1
833
+ readonly id: string
834
+ readonly title: string
835
+ readonly description?: string
836
+ readonly state: WorkspaceState
837
+ readonly tags?: readonly string[]
838
+ readonly metadata?: Readonly<Record<string, string>>
839
+ readonly masterMemberID?: string
840
+ readonly revision: number
841
+ readonly createdAt: number
842
+ readonly updatedAt: number
843
+ }
844
+
845
+ export interface WorkspaceBinding {
846
+ readonly v: 1
847
+ readonly id: string
848
+ readonly workspaceID: string
849
+ readonly machineID: string
850
+ readonly repo: string
851
+ readonly label?: string
852
+ readonly revision: number
853
+ }
854
+
855
+ export interface WorkspaceMemberRecord {
856
+ readonly v: 1
857
+ readonly id: string
858
+ readonly workspaceID: string
859
+ readonly sessionID: string
860
+ readonly role: WorkspaceRole
861
+ readonly generation: number
862
+ readonly joinedAt: number
863
+ readonly leftAt?: number
864
+ readonly leftReason?: "removed" | "replaced" | "ended" | "archived"
865
+ readonly revision: number
866
+ }
867
+
868
+ export interface WorkspaceMemberView {
869
+ readonly member: WorkspaceMemberRecord
870
+ readonly current: boolean
871
+ readonly presence: WorkspaceSessionPresence
872
+ }
873
+
874
+ export interface WorkspaceAssignmentRecord {
875
+ readonly v: 1
876
+ readonly id: string
877
+ readonly workspaceID: string
878
+ readonly memberID: string
879
+ readonly title: string
880
+ readonly status: WorkspaceAssignmentStatus
881
+ readonly ref?: string
882
+ readonly criteria?: readonly string[]
883
+ readonly revision: number
884
+ readonly createdAt: number
885
+ readonly updatedAt: number
886
+ readonly closedAt?: number
887
+ }
888
+
889
+ export interface WorkspaceAssignmentView {
890
+ readonly assignment: WorkspaceAssignmentRecord
891
+ readonly member: "current" | "left" | "missing"
892
+ readonly presence: WorkspaceSessionPresence
893
+ }
894
+
895
+ export interface WorkspaceSupervisionLink {
896
+ readonly v: 1
897
+ readonly id: string
898
+ readonly workspaceID: string
899
+ readonly supervisorMemberID: string
900
+ readonly memberID: string
901
+ readonly order: number
902
+ readonly revision: number
903
+ }
904
+
905
+ export interface WorkspaceSupervisionGraph {
906
+ readonly workspaceID: string
907
+ /** The graph revision a supervise call names as expectedRevision (0: never written). */
908
+ readonly revision: number
909
+ readonly links: readonly { readonly link: WorkspaceSupervisionLink; readonly member: "current" | "left" | "missing"; readonly supervisor: "current" | "left" | "missing" }[]
910
+ }
911
+
912
+ export interface WorkspaceSupervised {
913
+ readonly workspaceID: string
914
+ readonly revision: number
915
+ readonly change: "attached" | "moved" | "reordered" | "detached" | "unchanged"
916
+ readonly link?: WorkspaceSupervisionLink
917
+ readonly pruned: number
918
+ }
919
+
920
+ export interface WorkspaceDeparture {
921
+ readonly member: WorkspaceMemberRecord
922
+ readonly supervision: { readonly workspaceID: string; readonly revision: number; readonly detached: number; readonly transferred: number; readonly pruned: number }
923
+ }
924
+
925
+ /** GET /workspaces/:id/master (HWA-12): the current master, earlier masters (newest first) and a replacement in flight. */
926
+ export interface WorkspaceMasterView {
927
+ readonly workspaceID: string
928
+ readonly revision: number
929
+ readonly master?: { readonly member: WorkspaceMemberRecord; readonly presence: WorkspaceSessionPresence }
930
+ readonly previous: readonly { readonly member: WorkspaceMemberRecord; readonly presence: WorkspaceSessionPresence }[]
931
+ readonly pending?: { readonly phase: "claimed" | "joined"; readonly at: number; readonly stale: boolean }
932
+ }
933
+
934
+ /** POST /workspaces/:id/master (HWA-12). No grants move; the new master starts from a fresh digest snapshot. */
935
+ export interface WorkspaceMasterReplaced {
936
+ readonly workspace: WorkspaceView
937
+ readonly master: WorkspaceMemberRecord
938
+ readonly presence: WorkspaceSessionPresence
939
+ readonly previous?: WorkspaceMemberRecord
940
+ readonly previousPresence?: WorkspaceSessionPresence
941
+ /** False when the session already was the master. */
942
+ readonly changed: boolean
943
+ readonly supervision?: { readonly workspaceID: string; readonly revision: number; readonly detached: number; readonly transferred: number; readonly pruned: number }
944
+ readonly checkpoints: { readonly inherited: false; readonly previousCheckpoint?: { readonly id: string; readonly supervisorMemberID: string; readonly cursor: string; readonly at: number; readonly generation: number; readonly sessionID: string } }
945
+ readonly grants: { readonly transferred: false; readonly note: string }
946
+ }
947
+
948
+ export interface WorkspaceArchived {
949
+ readonly workspace: WorkspaceView
950
+ readonly departed: number
951
+ readonly supervision: { readonly detached: number }
952
+ }
953
+
954
+ export interface WorkspacePage<T> {
955
+ readonly items: readonly T[]
956
+ readonly cursor?: string
957
+ }
958
+
959
+ /** One section of the board an app may lack the scope for, that this hub doesn't serve, or whose source failed. */
960
+ export type WorkspaceBoardSection<T> =
961
+ | { readonly status: "ok"; readonly items: readonly T[]; readonly truncated: boolean }
962
+ | { readonly status: "omitted"; readonly reason: "scope"; readonly scope: string }
963
+ | { readonly status: "unsupported"; readonly feature: string }
964
+ | { readonly status: "failed" }
965
+
966
+ /**
967
+ * GET /workspaces/:id/board (HWA-10): a read-only, bounded situation snapshot. References and states only; detail
968
+ * text (prompts, Decision questions, blocker summaries, criteria, artifact content) stays behind its own routes.
969
+ */
970
+ export interface WorkspaceBoard {
971
+ readonly v: 1
972
+ readonly at: number
973
+ /** Open assignments without an update for this long carry `no_recent_update`. */
974
+ readonly staleAfterMs: number
975
+ readonly workspace: { readonly id: string; readonly title: string; readonly state: WorkspaceState; readonly revision: number; readonly masterMemberID?: string; readonly createdAt: number; readonly updatedAt: number }
976
+ readonly members: {
977
+ readonly items: readonly {
978
+ readonly memberID: string
979
+ readonly sessionID: string
980
+ readonly role: WorkspaceRole
981
+ readonly generation: number
982
+ readonly revision: number
983
+ readonly joinedAt: number
984
+ readonly master: boolean
985
+ readonly presence: WorkspaceSessionPresence
986
+ /** Absent when the hub doesn't know the session now. */
987
+ readonly session?: { readonly state: string; readonly online: boolean; readonly lastSeen?: number; readonly machineID?: string; readonly name?: string; readonly busySince?: number }
988
+ /** `reported: false`: this hub's catalog doesn't report the session now (not "no blockers"). */
989
+ readonly blockers: { readonly reported: false } | { readonly reported: true; readonly count: number; readonly items: readonly { readonly kind: string; readonly id: string; readonly sessionID?: string }[]; readonly truncated?: true }
990
+ readonly assignments: { readonly open: number; readonly in_progress: number; readonly blocked: number }
991
+ readonly decisions?: number
992
+ readonly stale?: Exclude<WorkspaceSessionPresence, "online">
993
+ }[]
994
+ readonly total: number
995
+ readonly truncated: boolean
996
+ }
997
+ readonly supervision: { readonly revision: number; readonly links: readonly { readonly memberID: string; readonly supervisorMemberID: string; readonly order: number; readonly member: "current" | "left" | "missing"; readonly supervisor: "current" | "left" | "missing" }[]; readonly truncated: boolean }
998
+ readonly assignments: {
999
+ readonly items: readonly {
1000
+ readonly id: string
1001
+ readonly memberID: string
1002
+ readonly title: string
1003
+ /** Exactly the stored status: never inferred from idleness or presence. */
1004
+ readonly status: WorkspaceAssignmentStatus
1005
+ readonly ref?: string
1006
+ readonly revision: number
1007
+ readonly createdAt: number
1008
+ readonly updatedAt: number
1009
+ readonly closedAt?: number
1010
+ readonly assignee: "current" | "left" | "missing"
1011
+ readonly stale?: readonly ("assignee_left" | "assignee_missing" | "assignee_offline" | "no_recent_update")[]
1012
+ }[]
1013
+ readonly counts: Readonly<Record<WorkspaceAssignmentStatus, number>>
1014
+ readonly truncated: boolean
1015
+ }
1016
+ readonly decisions: WorkspaceBoardSection<{ readonly id: string; readonly state: string; readonly urgency: string; readonly tier: string; readonly at: number; readonly expiresAt: number; readonly deadline?: number; readonly escalatedAt?: number; readonly sessionIDs: readonly string[]; readonly memberIDs: readonly string[]; readonly stale?: "past_deadline" }>
1017
+ readonly artifacts: WorkspaceBoardSection<{ readonly id: string; readonly kind: string; readonly visibility: string; readonly state: string; readonly revision: number; readonly latestRevisionID: string; readonly updatedAt: number }>
1018
+ }
1019
+
1020
+ /** One quoted source message of a side chat's context snapshot (HWA-13). */
1021
+ export interface WorkspaceSideChatItem {
1022
+ readonly kind: "message"
1023
+ readonly id: string
1024
+ readonly role: "user" | "assistant" | "message"
1025
+ readonly at?: number
1026
+ readonly pos?: number
1027
+ readonly text: string
1028
+ readonly bytes: number
1029
+ readonly clipped?: true
1030
+ /** Refresh markers: changed since the previous snapshot, or no longer readable (the previous copy is kept). */
1031
+ readonly changed?: true
1032
+ readonly stale?: "missing"
1033
+ }
1034
+
1035
+ /** A side chat's bounded context snapshot: exactly what the side-chat session was given. */
1036
+ export interface WorkspaceSideChatSnapshot {
1037
+ readonly generation: number
1038
+ readonly takenAt: number
1039
+ readonly mode: "selected" | "recent"
1040
+ readonly items: readonly WorkspaceSideChatItem[]
1041
+ readonly bytes: number
1042
+ readonly omitted: number
1043
+ readonly sourceNewestAt?: number
1044
+ }
1045
+
1046
+ interface WorkspaceSideChatBase {
1047
+ readonly v: 1
1048
+ readonly id: string
1049
+ readonly workspaceID: string
1050
+ /** The side-chat session, once its launch reports it. */
1051
+ readonly sessionID?: string
1052
+ readonly sourceSessionID: string
1053
+ readonly sources: readonly { readonly kind: "message" | "artifact_revision"; readonly id: string }[]
1054
+ readonly launchID: string
1055
+ readonly by: string
1056
+ readonly bindingID: string
1057
+ readonly machineID: string
1058
+ readonly harness: string
1059
+ readonly revision: number
1060
+ readonly createdAt: number
1061
+ readonly updatedAt: number
1062
+ /** ready: the side-chat session exists; launching: its launch is under way (or waiting for the machine). */
1063
+ readonly status: "launching" | "ready" | "failed" | "unknown"
1064
+ readonly launch?: { readonly id: string; readonly state: string; readonly outcome?: string; readonly reason?: string }
1065
+ /** When the hub knows the source's last change: newer than the snapshot = stale. */
1066
+ readonly sourceUpdatedAt?: number
1067
+ readonly stale?: boolean
1068
+ }
1069
+
1070
+ /** A side chat with its snapshot (create, get, refresh). */
1071
+ export interface WorkspaceSideChat extends WorkspaceSideChatBase {
1072
+ readonly snapshot: WorkspaceSideChatSnapshot
1073
+ }
1074
+
1075
+ /** A side chat in a list: the snapshot's summary, without quoted text. */
1076
+ export interface WorkspaceSideChatSummary extends WorkspaceSideChatBase {
1077
+ readonly snapshot: Omit<WorkspaceSideChatSnapshot, "items"> & { readonly count: number }
1078
+ }
1079
+
1080
+ /** How a refreshed context reached the side-chat session (a queued message). */
1081
+ export type WorkspaceSideChatDelivery =
1082
+ | { readonly status: "not_started" }
1083
+ | { readonly status: "sent"; readonly message: unknown; readonly delivery?: string }
1084
+ | { readonly status: "failed"; readonly code: string; readonly message: string }
@@ -0,0 +1,20 @@
1
+ // Why a message did not wake its recipient (Sent.woke / Sent.wakeSkipped, hub 0.8.1), in the words the CLI, `hub mcp`
2
+ // and the OpenCode hub tool show: one place so every client says the same thing.
3
+ import type { Sent, WakeSkip } from "./types.ts"
4
+
5
+ /** One short reason per WakeSkip. */
6
+ export const WAKE_SKIP_TEXT: Readonly<Record<WakeSkip, string>> = {
7
+ wake_false: "sent without a wake",
8
+ subagent: "subagents are never woken",
9
+ hop_cap: "hop limit reached (8 agent messages without a human turn)",
10
+ budget: "wake budget spent (30 per 10 minutes)",
11
+ }
12
+
13
+ /**
14
+ * ` — wake skipped: <reason>` when the hub did not wake the recipient and the message waits for a later turn; empty
15
+ * when it woke, went into a running turn (steer) or started one, or the hub did not say (hubs before 0.8.1).
16
+ */
17
+ export function wakeSkipNote({ delivery, woke, wakeSkipped }: Pick<Sent, "delivery" | "woke" | "wakeSkipped">): string {
18
+ if (woke !== false || !wakeSkipped || delivery === "steer" || delivery === "turn") return ""
19
+ return ` — wake skipped: ${WAKE_SKIP_TEXT[wakeSkipped] ?? wakeSkipped}`
20
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huynle/hub-spoke-sdk",
3
- "version": "0.7.4",
3
+ "version": "0.8.1",
4
4
  "private": true,
5
5
  "description": "Harness-neutral spoke building blocks: hub connection, enrollment, tokens, idempotent deliver, prompt references, HarnessSpoke (internal; shipped inside @huynle/hub, not published on its own).",
6
6
  "license": "MIT",
@@ -10,3 +10,4 @@ export * from "./session-tokens.ts"
10
10
  export * from "./session-ids.ts"
11
11
  export * from "./harness-hello.ts"
12
12
  export * from "./harness-spoke.ts"
13
+ export * from "./profile-apply.ts"