@mattstack/rt-client 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -74,6 +74,45 @@ doesn't error, it resolves empty. `parseIdentity` is strict — only strings
74
74
  `serializeIdentity` emitted parse — so validate payloads with it, and never
75
75
  hand-assemble or string-split a wire.
76
76
 
77
+ ## Chat
78
+
79
+ The `rt chat` surface the chat viewer (`~/Documents/GitHub/chat`) is built
80
+ on. Every call degrades to `{ ok: false, error }` instead of throwing, and a
81
+ daemon that is down and a daemon that refused look the same to the caller;
82
+ branch on `ok`, not on exceptions.
83
+
84
+ ```ts
85
+ import { chatRooms, chatMessages, chatPost, createRelay, daemonHealth } from '@mattstack/rt-client';
86
+
87
+ const rooms = await chatRooms({ handle: 'matt' }); // { ok, data: { rooms } }
88
+ const page = await chatMessages({ room: 'build', limit: 50 }); // newest page; `before: <id>` pages older
89
+ await chatPost({ handle: 'matt', room: 'build', body: 'on it' });
90
+
91
+ const health = await daemonHealth(); // { reachable, error? }, never throws
92
+ const stop = createRelay({ // one daemon subscription, republished
93
+ match: t => t.startsWith('chat/'), // `chat/<room>/msg`, `chat/wake/<handle>`
94
+ topic: 'chat',
95
+ publish: (topic, data) => server.publish(topic, data),
96
+ });
97
+ ```
98
+
99
+ | Function | Daemon verb |
100
+ | --- | --- |
101
+ | `chatSignIn` / `chatSignOut` / `chatAway` / `chatBack` / `chatPulse` | presence: the buddy-list row and its heartbeats |
102
+ | `chatBuddies` / `chatWho` / `chatRooms` | the roster, one room's members, a handle's rooms |
103
+ | `chatJoin` / `chatLeave` | membership (`wakeOn: mention \| all \| none`) |
104
+ | `chatPost` / `chatDm` / `chatRead` / `chatMessages` / `chatMark` | messages: post, DM, read-and-advance, page, advance the cursor |
105
+ | `chatArm` / `chatTouch` / `chatDisarm` / `chatUnreadWaking` | the tail's wake protocol; the CLI's `rt chat tail` uses these |
106
+ | `paneList` / `panePeek` / `paneSpawn` / `paneAccounts` / `paneDirectories` | herdr panes: list with presence joined, peek a screen, start claude in a tab, cswap accounts, directory suggestions |
107
+ | `chatInvite` | type `/chat:join <room>` into a pane; `accepted`, `queued` or `refused` |
108
+ | `createRelay` / `subscribe` | the event stream; `daemonHealth` the reachability probe |
109
+
110
+ Pass `{ sockPath }` as the trailing options to reach a non-default daemon
111
+ socket. The verbs, their payloads and the wake protocol are specified in
112
+ repo-tools `docs/superpowers/specs/2026-08-23-rt-chat-design.md` and
113
+ `2026-08-24-rt-chat-presence-design.md`; the agent-facing rules are
114
+ `skills/rt-chat/SKILL.md`.
115
+
77
116
  ## License
78
117
 
79
118
  MIT
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { RtResponse, RtClientOptions } from "./transport.ts";
2
- import type { DemandDecl, ProjectMRsData, DiscussionsData, MrByBranchData, BranchEnrichment, ForgeSlug, ForgeTokenData, RunSummary, RunDetail, WakeMode, ChatMember, ChatMessage, RoomSummary, BuddyStatus, PresenceRow } from "./commands.ts";
2
+ import type { DemandDecl, ProjectMRsData, DiscussionsData, MrByBranchData, BranchEnrichment, ForgeSlug, ForgeTokenData, RunSummary, RunDetail, WakeMode, ChatMember, ChatMessage, RoomSummary, BuddyStatus, PresenceRow, AgentRecord, Commands } from "./commands.ts";
3
3
  /**
4
4
  * One repo's project open-MR store. A cold repo forces a full paginated sync
5
5
  * on the daemon side when maxAgeMs demands it, which can run tens of seconds
@@ -102,6 +102,7 @@ export declare function chatArm(a: {
102
102
  }, o?: RtClientOptions): Promise<RtResponse<Record<string, never>>>;
103
103
  export declare function chatTouch(a: {
104
104
  handle: string;
105
+ room?: string;
105
106
  sessionId?: string;
106
107
  }, o?: RtClientOptions): Promise<RtResponse<Record<string, never>>>;
107
108
  export declare function chatDisarm(a: {
@@ -173,3 +174,20 @@ export declare function chatDm(a: {
173
174
  export declare function eventsHead(o?: RtClientOptions): Promise<RtResponse<{
174
175
  cursor: number;
175
176
  }>>;
177
+ export declare function agentStart(a: Commands["agent:start"]["payload"], o?: RtClientOptions): Promise<RtResponse<AgentRecord>>;
178
+ export declare function agentResume(a: Commands["agent:resume"]["payload"], o?: RtClientOptions): Promise<RtResponse<AgentRecord>>;
179
+ export declare function agentGet(a: {
180
+ id: string;
181
+ }, o?: RtClientOptions): Promise<RtResponse<AgentRecord>>;
182
+ export declare function agentList(a: {
183
+ repo?: string;
184
+ }, o?: RtClientOptions): Promise<RtResponse<{
185
+ agents: AgentRecord[];
186
+ }>>;
187
+ export declare function paneList(o?: RtClientOptions): Promise<RtResponse<Commands["pane:list"]["data"]>>;
188
+ export declare function panePeek(a: Commands["pane:peek"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["pane:peek"]["data"]>>;
189
+ /** The spawn waits on claude starting, so its budget is minutes, not seconds. */
190
+ export declare function paneSpawn(a: Commands["pane:spawn"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["pane:spawn"]["data"]>>;
191
+ export declare function paneAccounts(o?: RtClientOptions): Promise<RtResponse<Commands["pane:accounts"]["data"]>>;
192
+ export declare function paneDirectories(a: Commands["pane:directories"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["pane:directories"]["data"]>>;
193
+ export declare function chatInvite(a: Commands["chat:invite"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["chat:invite"]["data"]>>;
@@ -10,17 +10,24 @@ export type Discussion = MRDetail["discussions"][number];
10
10
  export interface DemandDecl {
11
11
  client: string;
12
12
  authors: string[];
13
+ /** Codeowner sections this client needs covered (spec: second demand axis). */
14
+ codeownerSections?: string[];
13
15
  declaredAt: number;
14
16
  }
15
17
  export interface ProjectMRsScope {
16
18
  authors: string[];
17
19
  windowDays: number;
18
20
  uncovered: string[];
21
+ /** Effective synced section union; absent from a pre-sections daemon. */
22
+ sections?: string[];
23
+ /** Demanded sections not yet swept for this client. */
24
+ uncoveredSections?: string[];
19
25
  }
20
26
  export interface ProjectMRsData {
21
27
  mrs: Record<string, {
22
28
  pr: PullRequest;
23
29
  fetchedAt: number;
30
+ codeownerSections?: string[];
24
31
  }>;
25
32
  listSyncedAt: number;
26
33
  source: "poll" | "events" | "mutation";
@@ -144,6 +151,39 @@ export interface PresenceRow {
144
151
  armedAt?: number;
145
152
  signedOutAt?: number;
146
153
  }
154
+ export type AgentStatus = "idle" | "working" | "blocked" | "done" | "unknown";
155
+ /** Duplicated shape on purpose (see EventsBusEvent above): mirrors the daemon's pane row, which rt-client cannot import. */
156
+ export interface ChatPane {
157
+ paneId: string;
158
+ workspace: string;
159
+ title?: string;
160
+ cwd?: string;
161
+ repo?: string;
162
+ branch?: string;
163
+ agentStatus: AgentStatus;
164
+ sessionId?: string;
165
+ presence?: {
166
+ handle: string;
167
+ status: BuddyStatus;
168
+ rooms: string[];
169
+ };
170
+ }
171
+ export interface PaneAccount {
172
+ slot: number;
173
+ email: string;
174
+ alias?: string;
175
+ headroom?: string;
176
+ }
177
+ export interface PaneDirectory {
178
+ path: string;
179
+ repo: string;
180
+ branch?: string;
181
+ }
182
+ export interface InviteResult {
183
+ paneId: string;
184
+ delivered: "accepted" | "queued" | "refused";
185
+ reason?: string;
186
+ }
147
187
  export type Attention = {
148
188
  needs: boolean;
149
189
  reason: "failed" | "stale" | "stranded" | "blocked" | null;
@@ -216,6 +256,29 @@ export interface RunDetail {
216
256
  decisions: RunDecisionRow[];
217
257
  schemaAhead: boolean;
218
258
  }
259
+ export type AgentSurface = "herdr" | "headless";
260
+ export interface AgentRecord {
261
+ id: string;
262
+ repo: string;
263
+ cwd: string;
264
+ provider: string;
265
+ surface: AgentSurface;
266
+ sessionId: string;
267
+ model?: string;
268
+ effort?: string;
269
+ account?: string;
270
+ label?: string;
271
+ caller?: string;
272
+ paneId?: string;
273
+ tabId?: string;
274
+ workspaceId?: string;
275
+ extraArgs?: string;
276
+ exitCode?: number;
277
+ resultPath?: string;
278
+ createdAt: number;
279
+ lastResumedAt?: number;
280
+ finishedAt?: number;
281
+ }
219
282
  export interface Commands {
220
283
  "project-mrs:read": {
221
284
  payload: {
@@ -449,6 +512,7 @@ export interface Commands {
449
512
  "chat:touch": {
450
513
  payload: {
451
514
  handle: string;
515
+ room?: string;
452
516
  sessionId?: string;
453
517
  };
454
518
  data: Record<string, never>;
@@ -547,6 +611,99 @@ export interface Commands {
547
611
  recipients: string[];
548
612
  };
549
613
  };
614
+ "agent:start": {
615
+ payload: {
616
+ repo: string;
617
+ cwd: string;
618
+ prompt?: string;
619
+ surface?: AgentSurface;
620
+ model?: string;
621
+ effort?: string;
622
+ account?: string;
623
+ label?: string;
624
+ caller?: string;
625
+ workspace?: string;
626
+ tab?: string;
627
+ extraArgs?: string;
628
+ };
629
+ data: AgentRecord;
630
+ };
631
+ "agent:resume": {
632
+ payload: {
633
+ id: string;
634
+ prompt?: string;
635
+ surface?: AgentSurface;
636
+ };
637
+ data: AgentRecord;
638
+ };
639
+ "agent:get": {
640
+ payload: {
641
+ id: string;
642
+ };
643
+ data: AgentRecord;
644
+ };
645
+ "agent:list": {
646
+ payload: {
647
+ repo?: string;
648
+ };
649
+ data: {
650
+ agents: AgentRecord[];
651
+ };
652
+ };
653
+ "chat:invite": {
654
+ payload: {
655
+ paneId: string;
656
+ room: string;
657
+ note?: string;
658
+ from: string;
659
+ callerPane?: string;
660
+ };
661
+ data: InviteResult;
662
+ };
663
+ "pane:list": {
664
+ payload: Record<string, never>;
665
+ data: {
666
+ panes: ChatPane[];
667
+ };
668
+ };
669
+ "pane:peek": {
670
+ payload: {
671
+ paneId: string;
672
+ lines?: number;
673
+ };
674
+ data: {
675
+ paneId: string;
676
+ lines: string[];
677
+ };
678
+ };
679
+ "pane:accounts": {
680
+ payload: Record<string, never>;
681
+ data: {
682
+ accounts: PaneAccount[];
683
+ };
684
+ };
685
+ "pane:directories": {
686
+ payload: {
687
+ q?: string;
688
+ };
689
+ data: {
690
+ directories: PaneDirectory[];
691
+ };
692
+ };
693
+ "pane:spawn": {
694
+ payload: {
695
+ cwd: string;
696
+ account?: string;
697
+ model?: string;
698
+ effort?: string;
699
+ prompt?: string;
700
+ workspace?: string;
701
+ };
702
+ data: {
703
+ pane: ChatPane;
704
+ ready: boolean;
705
+ };
706
+ };
550
707
  }
551
708
  export type CommandName = keyof Commands;
552
709
  export declare const COMMAND_NAMES: readonly CommandName[];
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { rtCommand, DEFAULT_SOCK } from "./transport.ts";
2
2
  export type { RtResponse, RtClientOptions } from "./transport.ts";
3
- export { readProjectMRs, readDiscussions, readMrsByBranch, readBranchCache, resolveForgeToken, listRuns, getRun, abandonRun, chatJoin, chatLeave, chatPost, chatRead, chatRooms, chatWho, chatMark, chatMessages, chatArm, chatTouch, chatDisarm, chatUnreadWaking, chatSignIn, chatSignOut, chatAway, chatBack, chatBuddies, chatPulse, chatDm, eventsHead, } from "./client.ts";
3
+ export { readProjectMRs, readDiscussions, readMrsByBranch, readBranchCache, resolveForgeToken, listRuns, getRun, abandonRun, chatJoin, chatLeave, chatPost, chatRead, chatRooms, chatWho, chatMark, chatMessages, chatArm, chatTouch, chatDisarm, chatUnreadWaking, chatSignIn, chatSignOut, chatAway, chatBack, chatBuddies, chatPulse, chatDm, eventsHead, agentStart, agentResume, agentGet, agentList, paneList, panePeek, paneSpawn, paneAccounts, paneDirectories, chatInvite, } from "./client.ts";
4
4
  export { COMMAND_NAMES } from "./commands.ts";
5
- export type { Discussion, DemandDecl, ProjectMRsScope, ProjectMRsData, DiscussionsData, MrByBranchEntry, MrByBranchData, BranchEnrichment, Commands, CommandName, ForgeSlug, ForgeTokenData, Attention, RunSummary, RunStageRow, RunFieldRow, RunDecisionRow, RunDetail, WakeMode, ChatMember, ChatMessage, RoomSummary, BuddyStatus, PresenceRow, } from "./commands.ts";
5
+ export type { Discussion, DemandDecl, ProjectMRsScope, ProjectMRsData, DiscussionsData, MrByBranchEntry, MrByBranchData, BranchEnrichment, Commands, CommandName, ForgeSlug, ForgeTokenData, Attention, RunSummary, RunStageRow, RunFieldRow, RunDecisionRow, RunDetail, WakeMode, ChatMember, ChatMessage, RoomSummary, BuddyStatus, PresenceRow, AgentRecord, AgentSurface, AgentStatus, ChatPane, PaneAccount, PaneDirectory, InviteResult, } from "./commands.ts";
6
6
  export { subscribe, createRelay, DEFAULT_WS_URL } from "./relay.ts";
7
7
  export type { RelayEventType } from "./relay.ts";
8
8
  export { daemonHealth } from "./health.ts";
package/dist/index.js CHANGED
@@ -124,6 +124,8 @@ function chatArm(a, o = {}) {
124
124
  }
125
125
  function chatTouch(a, o = {}) {
126
126
  const payload = { handle: a.handle };
127
+ if (a.room !== undefined)
128
+ payload.room = a.room;
127
129
  if (a.sessionId !== undefined)
128
130
  payload.sessionId = a.sessionId;
129
131
  return rtCommand("chat:touch", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
@@ -187,6 +189,64 @@ function chatDm(a, o = {}) {
187
189
  function eventsHead(o = {}) {
188
190
  return rtCommand("events:head", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
189
191
  }
192
+ function agentStart(a, o = {}) {
193
+ const payload = { repo: a.repo, cwd: a.cwd };
194
+ for (const k of ["prompt", "surface", "model", "effort", "account", "label", "caller", "workspace", "tab", "extraArgs"]) {
195
+ if (a[k] !== undefined)
196
+ payload[k] = a[k];
197
+ }
198
+ return rtCommand("agent:start", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30000 });
199
+ }
200
+ function agentResume(a, o = {}) {
201
+ const payload = { id: a.id };
202
+ if (a.prompt !== undefined)
203
+ payload.prompt = a.prompt;
204
+ if (a.surface !== undefined)
205
+ payload.surface = a.surface;
206
+ return rtCommand("agent:resume", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30000 });
207
+ }
208
+ function agentGet(a, o = {}) {
209
+ return rtCommand("agent:get", { id: a.id }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
210
+ }
211
+ function agentList(a, o = {}) {
212
+ const payload = {};
213
+ if (a.repo !== undefined)
214
+ payload.repo = a.repo;
215
+ return rtCommand("agent:list", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
216
+ }
217
+ function paneList(o = {}) {
218
+ return rtCommand("pane:list", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
219
+ }
220
+ function panePeek(a, o = {}) {
221
+ const payload = { paneId: a.paneId };
222
+ if (a.lines !== undefined)
223
+ payload.lines = a.lines;
224
+ return rtCommand("pane:peek", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
225
+ }
226
+ function paneSpawn(a, o = {}) {
227
+ const payload = { cwd: a.cwd };
228
+ for (const k of ["account", "model", "effort", "prompt", "workspace"])
229
+ if (a[k] !== undefined)
230
+ payload[k] = a[k];
231
+ return rtCommand("pane:spawn", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 90000 });
232
+ }
233
+ function paneAccounts(o = {}) {
234
+ return rtCommand("pane:accounts", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
235
+ }
236
+ function paneDirectories(a, o = {}) {
237
+ const payload = {};
238
+ if (a.q !== undefined)
239
+ payload.q = a.q;
240
+ return rtCommand("pane:directories", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
241
+ }
242
+ function chatInvite(a, o = {}) {
243
+ const payload = { paneId: a.paneId, room: a.room, from: a.from };
244
+ if (a.note !== undefined)
245
+ payload.note = a.note;
246
+ if (a.callerPane !== undefined)
247
+ payload.callerPane = a.callerPane;
248
+ return rtCommand("chat:invite", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30000 });
249
+ }
190
250
  // src/commands.ts
191
251
  var COMMAND_NAMES = [
192
252
  "project-mrs:read",
@@ -219,7 +279,17 @@ var COMMAND_NAMES = [
219
279
  "chat:back",
220
280
  "chat:buddies",
221
281
  "chat:pulse",
222
- "chat:dm"
282
+ "chat:dm",
283
+ "agent:start",
284
+ "agent:resume",
285
+ "agent:get",
286
+ "agent:list",
287
+ "chat:invite",
288
+ "pane:list",
289
+ "pane:peek",
290
+ "pane:accounts",
291
+ "pane:directories",
292
+ "pane:spawn"
223
293
  ];
224
294
  // src/relay.ts
225
295
  var DEFAULT_WS_URL = "ws://127.0.0.1:9401/ws";
@@ -659,7 +729,7 @@ var REGISTRY = [
659
729
  type: "array",
660
730
  scopes: ["team"],
661
731
  merge: "replace",
662
- description: "The board's full member roster, including hidden-by-default entries."
732
+ description: "The authors tab's roster: whose MRs the classic board lists, including hidden-by-default entries. Codeowners tabs list MRs from anyone."
663
733
  },
664
734
  {
665
735
  key: "board.title",
@@ -680,14 +750,14 @@ var REGISTRY = [
680
750
  type: "array",
681
751
  scopes: ["team"],
682
752
  merge: "replace",
683
- description: "Ticket key prefixes (e.g. RT, MAT) the board links out to Linear from an MR title."
753
+ description: "Ticket key prefixes (e.g. RT, MAT) that keep the authors tab to the team's own tickets and link out to Linear from an MR title; rows on a codeowners tab pass regardless of prefix."
684
754
  },
685
755
  {
686
756
  key: "board.slack",
687
757
  type: "object",
688
758
  scopes: ["team"],
689
759
  merge: "deep",
690
- description: "The board's Slack posting config (app id, client id, channel, callback port); client secrets stay out of this store."
760
+ description: "The board's Slack posting config (app id, client id, channel, callback port); channel is the default that a tab's slackChannel overrides per MR. Client secrets stay out of this store."
691
761
  },
692
762
  {
693
763
  key: "board.doctorSkill",
@@ -703,6 +773,13 @@ var REGISTRY = [
703
773
  merge: "replace",
704
774
  description: "Doctor skill the board's own API-tier triage sweep runs on your MRs; deliberately never resolved through a repo's skills.jsonc manifest. A sibling flat key of board.triage, not a field inside it — the board reader assembles the two independently."
705
775
  },
776
+ {
777
+ key: "board.tabs",
778
+ type: "array",
779
+ scopes: ["team"],
780
+ merge: "replace",
781
+ description: "Board tabs ({id, label, source, slackChannel?, reviewSkill?}), editable from the board's settings modal. source.kind 'authors' is the classic roster board; 'codeowners' lists MRs from any author blocked on an unapproved CODEOWNERS section. Absent = one implicit authors tab (fallback lives in the board reader, never here)."
782
+ },
706
783
  {
707
784
  key: "board.staleAfterDays",
708
785
  type: "number",
@@ -729,7 +806,7 @@ var REGISTRY = [
729
806
  type: "array",
730
807
  scopes: ["user"],
731
808
  merge: "replace",
732
- description: "Usernames this developer hides from the team roster's board.members list; overlays the team truth without editing it."
809
+ description: "Usernames this developer hides from the authors tab's board.members roster; overlays the team truth without editing it."
733
810
  },
734
811
  {
735
812
  key: "board.triage",
@@ -809,6 +886,21 @@ var REGISTRY = [
809
886
  merge: "replace",
810
887
  description: "The human's own chat handle, so agents can @-mention them by name."
811
888
  },
889
+ {
890
+ key: "chat.viewerUrl",
891
+ type: "string",
892
+ scopes: ["user"],
893
+ merge: "replace",
894
+ description: "Base URL of the chat viewer, no trailing slash. When set, rt chat post and the tail's wake lines end with a link to the room or message."
895
+ },
896
+ {
897
+ key: "chat.herdrWorkspace",
898
+ type: "string",
899
+ scopes: ["user"],
900
+ default: "chat",
901
+ merge: "replace",
902
+ description: "herdr workspace label that rt pane spawn opens new agent tabs in; created when missing."
903
+ },
812
904
  {
813
905
  key: "chat.push.provider",
814
906
  type: "string",
@@ -822,6 +914,34 @@ var REGISTRY = [
822
914
  scopes: ["user"],
823
915
  merge: "replace",
824
916
  description: "Destination (topic/URL/token) the configured chat.push.provider sends to."
917
+ },
918
+ {
919
+ key: "agent.model",
920
+ type: "string",
921
+ scopes: ["user", "machine"],
922
+ merge: "replace",
923
+ description: "Default --model for rt agent launches; unset omits the flag."
924
+ },
925
+ {
926
+ key: "agent.effort",
927
+ type: "string",
928
+ scopes: ["user", "machine"],
929
+ merge: "replace",
930
+ description: "Default --effort for rt agent launches; unset omits the flag."
931
+ },
932
+ {
933
+ key: "agent.account",
934
+ type: "string",
935
+ scopes: ["user", "machine"],
936
+ merge: "replace",
937
+ description: "cswap account email rt agent launches under; unset uses the default claude profile."
938
+ },
939
+ {
940
+ key: "agent.extraArgs",
941
+ type: "string",
942
+ scopes: ["user", "machine"],
943
+ merge: "replace",
944
+ description: "Opaque extra claude arguments appended to every rt agent launch (escape hatch)."
825
945
  }
826
946
  ];
827
947
 
@@ -1617,6 +1737,11 @@ export {
1617
1737
  readDiscussions,
1618
1738
  readBranchCache,
1619
1739
  parseIdentity,
1740
+ paneSpawn,
1741
+ panePeek,
1742
+ paneList,
1743
+ paneDirectories,
1744
+ paneAccounts,
1620
1745
  normalizeRemote,
1621
1746
  listTeams,
1622
1747
  listSettings,
@@ -1646,6 +1771,7 @@ export {
1646
1771
  chatMark,
1647
1772
  chatLeave,
1648
1773
  chatJoin,
1774
+ chatInvite,
1649
1775
  chatDm,
1650
1776
  chatDisarm,
1651
1777
  chatBuddies,
@@ -1653,6 +1779,10 @@ export {
1653
1779
  chatAway,
1654
1780
  chatArm,
1655
1781
  allDefs,
1782
+ agentStart,
1783
+ agentResume,
1784
+ agentList,
1785
+ agentGet,
1656
1786
  abandonRun,
1657
1787
  SCOPE_ORDER,
1658
1788
  REGISTRY,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mattstack/rt-client",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/client.ts CHANGED
@@ -21,6 +21,8 @@ import type {
21
21
  RoomSummary,
22
22
  BuddyStatus,
23
23
  PresenceRow,
24
+ AgentRecord,
25
+ Commands,
24
26
  } from "./commands.ts";
25
27
 
26
28
  /**
@@ -227,10 +229,11 @@ export function chatArm(
227
229
  }
228
230
 
229
231
  export function chatTouch(
230
- a: { handle: string; sessionId?: string },
232
+ a: { handle: string; room?: string; sessionId?: string },
231
233
  o: RtClientOptions = {},
232
234
  ): Promise<RtResponse<Record<string, never>>> {
233
235
  const payload: Record<string, unknown> = { handle: a.handle };
236
+ if (a.room !== undefined) payload.room = a.room;
234
237
  if (a.sessionId !== undefined) payload.sessionId = a.sessionId;
235
238
  return rtCommand<Record<string, never>>("chat:touch", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
236
239
  }
@@ -323,3 +326,77 @@ export function chatDm(
323
326
  export function eventsHead(o: RtClientOptions = {}): Promise<RtResponse<{ cursor: number }>> {
324
327
  return rtCommand<{ cursor: number }>("events:head", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
325
328
  }
329
+
330
+ // ─── Agent handoff (rt agent) ─────────────────────────────────────────────
331
+
332
+ export function agentStart(
333
+ a: Commands["agent:start"]["payload"], o: RtClientOptions = {},
334
+ ): Promise<RtResponse<AgentRecord>> {
335
+ const payload: Record<string, unknown> = { repo: a.repo, cwd: a.cwd };
336
+ for (const k of ["prompt", "surface", "model", "effort", "account", "label", "caller", "workspace", "tab", "extraArgs"] as const) {
337
+ if (a[k] !== undefined) payload[k] = a[k];
338
+ }
339
+ return rtCommand<AgentRecord>("agent:start", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30_000 });
340
+ }
341
+
342
+ export function agentResume(
343
+ a: Commands["agent:resume"]["payload"], o: RtClientOptions = {},
344
+ ): Promise<RtResponse<AgentRecord>> {
345
+ const payload: Record<string, unknown> = { id: a.id };
346
+ if (a.prompt !== undefined) payload.prompt = a.prompt;
347
+ if (a.surface !== undefined) payload.surface = a.surface;
348
+ return rtCommand<AgentRecord>("agent:resume", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30_000 });
349
+ }
350
+
351
+ export function agentGet(a: { id: string }, o: RtClientOptions = {}): Promise<RtResponse<AgentRecord>> {
352
+ return rtCommand<AgentRecord>("agent:get", { id: a.id }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
353
+ }
354
+
355
+ export function agentList(a: { repo?: string }, o: RtClientOptions = {}): Promise<RtResponse<{ agents: AgentRecord[] }>> {
356
+ const payload: Record<string, unknown> = {};
357
+ if (a.repo !== undefined) payload.repo = a.repo;
358
+ return rtCommand<{ agents: AgentRecord[] }>("agent:list", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
359
+ }
360
+
361
+ // ─── Panes (rt chat invite) ────────────────────────────────────────────────
362
+ // herdr-facing verbs; the daemon answers `herdr unavailable` without herdr.
363
+
364
+ export function paneList(o: RtClientOptions = {}): Promise<RtResponse<Commands["pane:list"]["data"]>> {
365
+ return rtCommand<Commands["pane:list"]["data"]>("pane:list", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
366
+ }
367
+
368
+ export function panePeek(a: Commands["pane:peek"]["payload"], o: RtClientOptions = {}): Promise<RtResponse<Commands["pane:peek"]["data"]>> {
369
+ const payload: Record<string, unknown> = { paneId: a.paneId };
370
+ if (a.lines !== undefined) payload.lines = a.lines;
371
+ return rtCommand<Commands["pane:peek"]["data"]>("pane:peek", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
372
+ }
373
+
374
+ /** The spawn waits on claude starting, so its budget is minutes, not seconds. */
375
+ export function paneSpawn(
376
+ a: Commands["pane:spawn"]["payload"],
377
+ o: RtClientOptions = {},
378
+ ): Promise<RtResponse<Commands["pane:spawn"]["data"]>> {
379
+ const payload: Record<string, unknown> = { cwd: a.cwd };
380
+ for (const k of ["account", "model", "effort", "prompt", "workspace"] as const) if (a[k] !== undefined) payload[k] = a[k];
381
+ return rtCommand<Commands["pane:spawn"]["data"]>("pane:spawn", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 90_000 });
382
+ }
383
+
384
+ export function paneAccounts(o: RtClientOptions = {}): Promise<RtResponse<Commands["pane:accounts"]["data"]>> {
385
+ return rtCommand<Commands["pane:accounts"]["data"]>("pane:accounts", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
386
+ }
387
+
388
+ export function paneDirectories(a: Commands["pane:directories"]["payload"], o: RtClientOptions = {}): Promise<RtResponse<Commands["pane:directories"]["data"]>> {
389
+ const payload: Record<string, unknown> = {};
390
+ if (a.q !== undefined) payload.q = a.q;
391
+ return rtCommand<Commands["pane:directories"]["data"]>("pane:directories", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
392
+ }
393
+
394
+ export function chatInvite(
395
+ a: Commands["chat:invite"]["payload"],
396
+ o: RtClientOptions = {},
397
+ ): Promise<RtResponse<Commands["chat:invite"]["data"]>> {
398
+ const payload: Record<string, unknown> = { paneId: a.paneId, room: a.room, from: a.from };
399
+ if (a.note !== undefined) payload.note = a.note;
400
+ if (a.callerPane !== undefined) payload.callerPane = a.callerPane;
401
+ return rtCommand<Commands["chat:invite"]["data"]>("chat:invite", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30_000 });
402
+ }
package/src/commands.ts CHANGED
@@ -12,6 +12,8 @@ export type Discussion = MRDetail["discussions"][number];
12
12
  export interface DemandDecl {
13
13
  client: string;
14
14
  authors: string[];
15
+ /** Codeowner sections this client needs covered (spec: second demand axis). */
16
+ codeownerSections?: string[];
15
17
  declaredAt: number;
16
18
  }
17
19
 
@@ -19,10 +21,14 @@ export interface ProjectMRsScope {
19
21
  authors: string[];
20
22
  windowDays: number;
21
23
  uncovered: string[];
24
+ /** Effective synced section union; absent from a pre-sections daemon. */
25
+ sections?: string[];
26
+ /** Demanded sections not yet swept for this client. */
27
+ uncoveredSections?: string[];
22
28
  }
23
29
 
24
30
  export interface ProjectMRsData {
25
- mrs: Record<string, { pr: PullRequest; fetchedAt: number }>;
31
+ mrs: Record<string, { pr: PullRequest; fetchedAt: number; codeownerSections?: string[] }>;
26
32
  listSyncedAt: number;
27
33
  source: "poll" | "events" | "mutation";
28
34
  syncedAt: number;
@@ -140,6 +146,25 @@ export interface PresenceRow {
140
146
  signedOutAt?: number;
141
147
  }
142
148
 
149
+ export type AgentStatus = "idle" | "working" | "blocked" | "done" | "unknown";
150
+
151
+ /** Duplicated shape on purpose (see EventsBusEvent above): mirrors the daemon's pane row, which rt-client cannot import. */
152
+ export interface ChatPane {
153
+ paneId: string;
154
+ workspace: string;
155
+ title?: string;
156
+ cwd?: string;
157
+ repo?: string;
158
+ branch?: string;
159
+ agentStatus: AgentStatus;
160
+ sessionId?: string;
161
+ presence?: { handle: string; status: BuddyStatus; rooms: string[] };
162
+ }
163
+
164
+ export interface PaneAccount { slot: number; email: string; alias?: string; headroom?: string }
165
+ export interface PaneDirectory { path: string; repo: string; branch?: string }
166
+ export interface InviteResult { paneId: string; delivered: "accepted" | "queued" | "refused"; reason?: string }
167
+
143
168
  // SKILLS-53: one judgment, computed once in rt, so the console and the tray
144
169
  // never derive two verdicts that can disagree.
145
170
  export type Attention = {
@@ -186,6 +211,18 @@ export interface RunFieldRow { key: string; value: string; produced_by: string;
186
211
  export interface RunDecisionRow { contract: string; scope: string; selection: string; decided_by: string; decided_at: number; }
187
212
  export interface RunDetail { run: RunSummary; stages: RunStageRow[]; fields: RunFieldRow[]; decisions: RunDecisionRow[]; schemaAhead: boolean; }
188
213
 
214
+ export type AgentSurface = "herdr" | "headless";
215
+
216
+ export interface AgentRecord {
217
+ id: string; repo: string; cwd: string; provider: string;
218
+ surface: AgentSurface; sessionId: string;
219
+ model?: string; effort?: string; account?: string;
220
+ label?: string; caller?: string;
221
+ paneId?: string; tabId?: string; workspaceId?: string;
222
+ extraArgs?: string; exitCode?: number; resultPath?: string;
223
+ createdAt: number; lastResumedAt?: number; finishedAt?: number;
224
+ }
225
+
189
226
  export interface Commands {
190
227
  "project-mrs:read": { payload: { repoName: string; maxAgeMs?: number; demand?: DemandDecl }; data: ProjectMRsData };
191
228
  "discussions:read": { payload: { repoName: string; iid: number }; data: DiscussionsData };
@@ -251,7 +288,7 @@ export interface Commands {
251
288
  "chat:mark": { payload: { handle: string; room?: string }; data: Record<string, never> };
252
289
  "chat:messages": { payload: { room: string; before?: number; limit?: number }; data: { messages: ChatMessage[] } };
253
290
  "chat:arm": { payload: { handle: string; room?: string; sessionId?: string }; data: Record<string, never> };
254
- "chat:touch": { payload: { handle: string; sessionId?: string }; data: Record<string, never> };
291
+ "chat:touch": { payload: { handle: string; room?: string; sessionId?: string }; data: Record<string, never> };
255
292
  "chat:disarm": { payload: { handle: string; sessionId?: string }; data: Record<string, never> };
256
293
  "chat:unread-waking": { payload: { handle: string; room?: string }; data: { rooms: { room: string; count: number; mentions: number; maxId: number }[] } };
257
294
 
@@ -271,6 +308,21 @@ export interface Commands {
271
308
  data: { unread: { dms: number; mentions: number; rooms: number }; status: BuddyStatus };
272
309
  };
273
310
  "chat:dm": { payload: { from: string; to: string; body: string; sessionId?: string }; data: { room: string; id: number; recipients: string[] } };
311
+
312
+ // ─── Agent handoff (rt agent) ────────────────────────────────────────────
313
+ "agent:start": { payload: { repo: string; cwd: string; prompt?: string; surface?: AgentSurface; model?: string; effort?: string; account?: string; label?: string; caller?: string; workspace?: string; tab?: string; extraArgs?: string }; data: AgentRecord };
314
+ "agent:resume": { payload: { id: string; prompt?: string; surface?: AgentSurface }; data: AgentRecord };
315
+ "agent:get": { payload: { id: string }; data: AgentRecord };
316
+ "agent:list": { payload: { repo?: string }; data: { agents: AgentRecord[] } };
317
+ "chat:invite": { payload: { paneId: string; room: string; note?: string; from: string; callerPane?: string }; data: InviteResult };
318
+ "pane:list": { payload: Record<string, never>; data: { panes: ChatPane[] } };
319
+ "pane:peek": { payload: { paneId: string; lines?: number }; data: { paneId: string; lines: string[] } };
320
+ "pane:accounts": { payload: Record<string, never>; data: { accounts: PaneAccount[] } };
321
+ "pane:directories": { payload: { q?: string }; data: { directories: PaneDirectory[] } };
322
+ "pane:spawn": {
323
+ payload: { cwd: string; account?: string; model?: string; effort?: string; prompt?: string; workspace?: string };
324
+ data: { pane: ChatPane; ready: boolean };
325
+ };
274
326
  }
275
327
 
276
328
  export type CommandName = keyof Commands;
@@ -307,4 +359,14 @@ export const COMMAND_NAMES: readonly CommandName[] = [
307
359
  "chat:buddies",
308
360
  "chat:pulse",
309
361
  "chat:dm",
362
+ "agent:start",
363
+ "agent:resume",
364
+ "agent:get",
365
+ "agent:list",
366
+ "chat:invite",
367
+ "pane:list",
368
+ "pane:peek",
369
+ "pane:accounts",
370
+ "pane:directories",
371
+ "pane:spawn",
310
372
  ];
package/src/index.ts CHANGED
@@ -30,6 +30,16 @@ export {
30
30
  chatPulse,
31
31
  chatDm,
32
32
  eventsHead,
33
+ agentStart,
34
+ agentResume,
35
+ agentGet,
36
+ agentList,
37
+ paneList,
38
+ panePeek,
39
+ paneSpawn,
40
+ paneAccounts,
41
+ paneDirectories,
42
+ chatInvite,
33
43
  } from "./client.ts";
34
44
 
35
45
  export { COMMAND_NAMES } from "./commands.ts";
@@ -58,6 +68,13 @@ export type {
58
68
  RoomSummary,
59
69
  BuddyStatus,
60
70
  PresenceRow,
71
+ AgentRecord,
72
+ AgentSurface,
73
+ AgentStatus,
74
+ ChatPane,
75
+ PaneAccount,
76
+ PaneDirectory,
77
+ InviteResult,
61
78
  } from "./commands.ts";
62
79
 
63
80
  export { subscribe, createRelay, DEFAULT_WS_URL } from "./relay.ts";
@@ -308,7 +308,7 @@ export const REGISTRY: readonly SettingDef[] = [
308
308
  type: "array",
309
309
  scopes: ["team"],
310
310
  merge: "replace",
311
- description: "The board's full member roster, including hidden-by-default entries.",
311
+ description: "The authors tab's roster: whose MRs the classic board lists, including hidden-by-default entries. Codeowners tabs list MRs from anyone.",
312
312
  },
313
313
  {
314
314
  key: "board.title",
@@ -329,14 +329,14 @@ export const REGISTRY: readonly SettingDef[] = [
329
329
  type: "array",
330
330
  scopes: ["team"],
331
331
  merge: "replace",
332
- description: "Ticket key prefixes (e.g. RT, MAT) the board links out to Linear from an MR title.",
332
+ description: "Ticket key prefixes (e.g. RT, MAT) that keep the authors tab to the team's own tickets and link out to Linear from an MR title; rows on a codeowners tab pass regardless of prefix.",
333
333
  },
334
334
  {
335
335
  key: "board.slack",
336
336
  type: "object",
337
337
  scopes: ["team"],
338
338
  merge: "deep",
339
- description: "The board's Slack posting config (app id, client id, channel, callback port); client secrets stay out of this store.",
339
+ description: "The board's Slack posting config (app id, client id, channel, callback port); channel is the default that a tab's slackChannel overrides per MR. Client secrets stay out of this store.",
340
340
  },
341
341
  {
342
342
  key: "board.doctorSkill",
@@ -352,6 +352,13 @@ export const REGISTRY: readonly SettingDef[] = [
352
352
  merge: "replace",
353
353
  description: "Doctor skill the board's own API-tier triage sweep runs on your MRs; deliberately never resolved through a repo's skills.jsonc manifest. A sibling flat key of board.triage, not a field inside it — the board reader assembles the two independently.",
354
354
  },
355
+ {
356
+ key: "board.tabs",
357
+ type: "array",
358
+ scopes: ["team"],
359
+ merge: "replace",
360
+ description: "Board tabs ({id, label, source, slackChannel?, reviewSkill?}), editable from the board's settings modal. source.kind 'authors' is the classic roster board; 'codeowners' lists MRs from any author blocked on an unapproved CODEOWNERS section. Absent = one implicit authors tab (fallback lives in the board reader, never here).",
361
+ },
355
362
 
356
363
  // --- board (user) ----------------------------------------------------------
357
364
  {
@@ -380,7 +387,7 @@ export const REGISTRY: readonly SettingDef[] = [
380
387
  type: "array",
381
388
  scopes: ["user"],
382
389
  merge: "replace",
383
- description: "Usernames this developer hides from the team roster's board.members list; overlays the team truth without editing it.",
390
+ description: "Usernames this developer hides from the authors tab's board.members roster; overlays the team truth without editing it.",
384
391
  },
385
392
  {
386
393
  key: "board.triage",
@@ -466,6 +473,21 @@ export const REGISTRY: readonly SettingDef[] = [
466
473
  merge: "replace",
467
474
  description: "The human's own chat handle, so agents can @-mention them by name.",
468
475
  },
476
+ {
477
+ key: "chat.viewerUrl",
478
+ type: "string",
479
+ scopes: ["user"],
480
+ merge: "replace",
481
+ description: "Base URL of the chat viewer, no trailing slash. When set, rt chat post and the tail's wake lines end with a link to the room or message.",
482
+ },
483
+ {
484
+ key: "chat.herdrWorkspace",
485
+ type: "string",
486
+ scopes: ["user"],
487
+ default: "chat",
488
+ merge: "replace",
489
+ description: "herdr workspace label that rt pane spawn opens new agent tabs in; created when missing.",
490
+ },
469
491
  {
470
492
  key: "chat.push.provider",
471
493
  type: "string",
@@ -480,4 +502,36 @@ export const REGISTRY: readonly SettingDef[] = [
480
502
  merge: "replace",
481
503
  description: "Destination (topic/URL/token) the configured chat.push.provider sends to.",
482
504
  },
505
+
506
+ // --- agent (rt agent handoff) --------------------------------------------
507
+ // No defaults by design: an unset key means the flag is omitted from the
508
+ // claude invocation entirely (spec "Settings").
509
+ {
510
+ key: "agent.model",
511
+ type: "string",
512
+ scopes: ["user", "machine"],
513
+ merge: "replace",
514
+ description: "Default --model for rt agent launches; unset omits the flag.",
515
+ },
516
+ {
517
+ key: "agent.effort",
518
+ type: "string",
519
+ scopes: ["user", "machine"],
520
+ merge: "replace",
521
+ description: "Default --effort for rt agent launches; unset omits the flag.",
522
+ },
523
+ {
524
+ key: "agent.account",
525
+ type: "string",
526
+ scopes: ["user", "machine"],
527
+ merge: "replace",
528
+ description: "cswap account email rt agent launches under; unset uses the default claude profile.",
529
+ },
530
+ {
531
+ key: "agent.extraArgs",
532
+ type: "string",
533
+ scopes: ["user", "machine"],
534
+ merge: "replace",
535
+ description: "Opaque extra claude arguments appended to every rt agent launch (escape hatch).",
536
+ },
483
537
  ];