@mattstack/rt-client 0.13.0 → 0.15.0

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/dist/client.d.ts CHANGED
@@ -186,3 +186,15 @@ export declare function paneSend(a: Commands["pane:send"]["payload"], o?: RtClie
186
186
  /** Brings a herdr pane to the front. The daemon routes this to the tray, which
187
187
  owns the herdr focus and the native terminal-window raise. */
188
188
  export declare function paneFocus(a: Commands["pane:focus"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["pane:focus"]["data"]>>;
189
+ export declare function gateOpen(a: Commands["gate:open"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:open"]["data"]>>;
190
+ export declare function gateAnswer(a: Commands["gate:answer"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:answer"]["data"]>>;
191
+ /** Daemon clamps its own wait to 240s (gates-store.ts); the client abort
192
+ must outlive that cap, same +10s buffer as commands/events.ts's
193
+ IPC_TIMEOUT_MS over DAEMON_WAIT_MS. */
194
+ export declare function gateWait(a: Commands["gate:wait"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:wait"]["data"]>>;
195
+ export declare function gateList(a: Commands["gate:list"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:list"]["data"]>>;
196
+ export declare function gatePark(a: Commands["gate:park"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:park"]["data"]>>;
197
+ export declare function gateClose(a: Commands["gate:close"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:close"]["data"]>>;
198
+ export declare function gateSubscribe(a: Commands["gate:subscribe"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:subscribe"]["data"]>>;
199
+ export declare function gateUnsubscribe(a: Commands["gate:unsubscribe"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:unsubscribe"]["data"]>>;
200
+ export declare function gateSubscriptions(a: Commands["gate:subscriptions"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:subscriptions"]["data"]>>;
@@ -91,6 +91,65 @@ export interface EventsBusEvent {
91
91
  payload: unknown;
92
92
  emittedAt: number;
93
93
  }
94
+ /**
95
+ * Duplicated shape on purpose, same reasoning as EventsBusEvent above:
96
+ * these mirror lib/daemon/gates-store.ts's types, which rt-client cannot
97
+ * import. gates-store.ts imports them back FROM this package (Commands
98
+ * already flows daemon -> rt-client, e.g. handlers/events.ts), so this is
99
+ * the single source of truth for the wire shape.
100
+ */
101
+ /** The `by` value a pane spells when it answers its own gate: the only
102
+ value gates-store.ts's release tracking (CAS winner or loser) reacts to. */
103
+ export declare const GATE_BY_PANE = "pane";
104
+ export type GateStatus = "open" | "answered" | "parked" | "closed";
105
+ export interface GateQuestion {
106
+ id: string;
107
+ label: string;
108
+ multi: boolean;
109
+ options: string[];
110
+ }
111
+ export interface GateAnswer {
112
+ answers: Record<string, string | string[] | {
113
+ value: string | string[];
114
+ note?: string;
115
+ }>;
116
+ by: string;
117
+ answeredAt: number;
118
+ }
119
+ export interface GateRow {
120
+ id: string;
121
+ subject: string;
122
+ kind: string;
123
+ questions: GateQuestion[];
124
+ meta: Record<string, unknown> | null;
125
+ status: GateStatus;
126
+ answer: GateAnswer | null;
127
+ openedAt: number;
128
+ parkedAt: number | null;
129
+ closedAt: number | null;
130
+ closedReason: "abandoned" | "superseded" | "pruned" | null;
131
+ agent: string | null;
132
+ pane: string | null;
133
+ nudge: {
134
+ session: string;
135
+ } | null;
136
+ delivery: {
137
+ outcome: "delivered" | "dead-pane";
138
+ at: number;
139
+ } | null;
140
+ released: boolean;
141
+ }
142
+ export interface GateSubscription {
143
+ id: string;
144
+ subjectPrefix: string;
145
+ session: string;
146
+ createdAt: number;
147
+ lastDelivery: {
148
+ outcome: "delivered" | "failed";
149
+ at: number;
150
+ } | null;
151
+ dead: boolean;
152
+ }
94
153
  /**
95
154
  * Duplicated shape on purpose, same reasoning as EventsBusEvent above:
96
155
  * these mirror lib/state/chat-store.ts's types, which rt-client cannot
@@ -1205,6 +1264,114 @@ export interface Commands {
1205
1264
  payload: Record<string, never>;
1206
1265
  data: unknown;
1207
1266
  };
1267
+ "gate:open": {
1268
+ payload: {
1269
+ subject: string;
1270
+ kind: string;
1271
+ questions: GateQuestion[];
1272
+ meta?: Record<string, unknown>;
1273
+ agent?: string;
1274
+ pane?: string;
1275
+ nudge?: {
1276
+ session: string;
1277
+ };
1278
+ };
1279
+ data: {
1280
+ id: string;
1281
+ supersededId: string | null;
1282
+ };
1283
+ };
1284
+ /**
1285
+ * A CAS loss is a DEFINED OUTCOME, not an error: `ok:true` with
1286
+ * `conflict:true` and the WINNING row, so every consumer gets the winner
1287
+ * typed with no envelope hacks. `ok:false` is reserved for
1288
+ * not-found/closed/validation failures.
1289
+ */
1290
+ "gate:answer": {
1291
+ payload: {
1292
+ id: string;
1293
+ answers: GateAnswer["answers"];
1294
+ by: string;
1295
+ };
1296
+ data: {
1297
+ row: GateRow;
1298
+ conflict?: true;
1299
+ };
1300
+ };
1301
+ /** `ok:false "not-found"` on an unknown id is terminal; the CLI loop must not re-enter on it.
1302
+ * `timeout` carries no row (nothing settled); `answered`/`closed` always carry the settled row. */
1303
+ "gate:wait": {
1304
+ payload: {
1305
+ id: string;
1306
+ waitMs?: number;
1307
+ };
1308
+ data: {
1309
+ status: "timeout";
1310
+ } | {
1311
+ status: "answered" | "closed";
1312
+ row: GateRow;
1313
+ };
1314
+ };
1315
+ /** Paged like events:list: an omitted `limit` clamps daemon-side rather than
1316
+ * forcing a full-table read; `cursor` is the paging rowid to resume from. */
1317
+ "gate:list": {
1318
+ payload: {
1319
+ open?: boolean;
1320
+ subjectPrefix?: string;
1321
+ kind?: string;
1322
+ limit?: number;
1323
+ cursor?: number;
1324
+ };
1325
+ data: {
1326
+ gates: GateRow[];
1327
+ cursor: number;
1328
+ };
1329
+ };
1330
+ "gate:park": {
1331
+ payload: {
1332
+ id: string;
1333
+ };
1334
+ data: {
1335
+ ok: true;
1336
+ };
1337
+ };
1338
+ "gate:close": {
1339
+ payload: {
1340
+ id: string;
1341
+ reason: "abandoned" | "superseded" | "pruned";
1342
+ };
1343
+ data: {
1344
+ ok: true;
1345
+ };
1346
+ };
1347
+ "gate:subscribe": {
1348
+ payload: {
1349
+ subjectPrefix: string;
1350
+ session: string;
1351
+ };
1352
+ data: {
1353
+ id: string;
1354
+ };
1355
+ };
1356
+ "gate:unsubscribe": {
1357
+ payload: {
1358
+ id: string;
1359
+ };
1360
+ data: {
1361
+ removed: boolean;
1362
+ };
1363
+ };
1364
+ /** The shepherd's gap-recovery liveness check and the observability window
1365
+ * onto delivery outcomes (dead marks included). */
1366
+ "gate:subscriptions": {
1367
+ payload: {
1368
+ session?: string;
1369
+ live?: boolean;
1370
+ };
1371
+ data: {
1372
+ subscriptions: GateSubscription[];
1373
+ };
1374
+ };
1208
1375
  /** Wire reply on success is always `{ok:true, repaired}` (no `data`
1209
1376
  * wrapper) — `data` here documents the extra field the same way PingData
1210
1377
  * does for `ping`, not the literal wire nesting (R3). */
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, chatAck, chatClaim, chatRelease, chatPost, chatRead, chatRooms, chatWho, chatMark, chatMessages, chatSignIn, chatSignOut, chatAway, chatBack, chatBuddies, chatDm, chatArchive, chatDmOpen, eventsHead, eventsEmit, eventsWait, eventsList, agentStart, agentResume, agentGet, agentList, paneList, panePeek, paneSpawn, paneAccounts, paneDirectories, chatInvite, paneSend, paneFocus, } from "./client.ts";
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, ChatClaimOutcome, RoomSummary, BuddyStatus, PresenceRow, AgentRecord, AgentSurface, AgentStatus, ChatPane, PaneAccount, PaneDirectory, InviteResult, PaneDelivery, PaneSendResult, PaneFocusResult, } from "./commands.ts";
3
+ export { readProjectMRs, readDiscussions, readMrsByBranch, readBranchCache, resolveForgeToken, listRuns, getRun, abandonRun, chatJoin, chatLeave, chatAck, chatClaim, chatRelease, chatPost, chatRead, chatRooms, chatWho, chatMark, chatMessages, chatSignIn, chatSignOut, chatAway, chatBack, chatBuddies, chatDm, chatArchive, chatDmOpen, eventsHead, eventsEmit, eventsWait, eventsList, agentStart, agentResume, agentGet, agentList, paneList, panePeek, paneSpawn, paneAccounts, paneDirectories, chatInvite, paneSend, paneFocus, gateOpen, gateAnswer, gateWait, gateList, gatePark, gateClose, gateSubscribe, gateUnsubscribe, gateSubscriptions, } from "./client.ts";
4
+ export { COMMAND_NAMES, GATE_BY_PANE } 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, ChatClaimOutcome, RoomSummary, BuddyStatus, PresenceRow, AgentRecord, AgentSurface, AgentStatus, ChatPane, PaneAccount, PaneDirectory, InviteResult, PaneDelivery, PaneSendResult, PaneFocusResult, GateStatus, GateQuestion, GateAnswer, GateRow, GateSubscription, } 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
@@ -268,7 +268,50 @@ function paneSend(a, o = {}) {
268
268
  function paneFocus(a, o = {}) {
269
269
  return rtCommand("pane:focus", { paneId: a.paneId }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
270
270
  }
271
+ function gateOpen(a, o = {}) {
272
+ const payload = { subject: a.subject, kind: a.kind, questions: a.questions };
273
+ for (const k of ["meta", "agent", "pane", "nudge"])
274
+ if (a[k] !== undefined)
275
+ payload[k] = a[k];
276
+ return rtCommand("gate:open", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
277
+ }
278
+ function gateAnswer(a, o = {}) {
279
+ return rtCommand("gate:answer", { id: a.id, answers: a.answers, by: a.by }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
280
+ }
281
+ function gateWait(a, o = {}) {
282
+ const payload = { id: a.id };
283
+ if (a.waitMs !== undefined)
284
+ payload.waitMs = a.waitMs;
285
+ return rtCommand("gate:wait", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 250000 });
286
+ }
287
+ function gateList(a, o = {}) {
288
+ const payload = {};
289
+ for (const k of ["open", "subjectPrefix", "kind", "limit", "cursor"])
290
+ if (a[k] !== undefined)
291
+ payload[k] = a[k];
292
+ return rtCommand("gate:list", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
293
+ }
294
+ function gatePark(a, o = {}) {
295
+ return rtCommand("gate:park", { id: a.id }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
296
+ }
297
+ function gateClose(a, o = {}) {
298
+ return rtCommand("gate:close", { id: a.id, reason: a.reason }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
299
+ }
300
+ function gateSubscribe(a, o = {}) {
301
+ return rtCommand("gate:subscribe", { subjectPrefix: a.subjectPrefix, session: a.session }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
302
+ }
303
+ function gateUnsubscribe(a, o = {}) {
304
+ return rtCommand("gate:unsubscribe", { id: a.id }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
305
+ }
306
+ function gateSubscriptions(a, o = {}) {
307
+ const payload = {};
308
+ for (const k of ["session", "live"])
309
+ if (a[k] !== undefined)
310
+ payload[k] = a[k];
311
+ return rtCommand("gate:subscriptions", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
312
+ }
271
313
  // src/commands.ts
314
+ var GATE_BY_PANE = "pane";
272
315
  var COMMAND_NAMES = [
273
316
  "project-mrs:read",
274
317
  "discussions:read",
@@ -337,6 +380,15 @@ var COMMAND_NAMES = [
337
380
  "endpoint:status",
338
381
  "repos:locate",
339
382
  "freshness:reconcile",
383
+ "gate:open",
384
+ "gate:answer",
385
+ "gate:wait",
386
+ "gate:list",
387
+ "gate:park",
388
+ "gate:close",
389
+ "gate:subscribe",
390
+ "gate:unsubscribe",
391
+ "gate:subscriptions",
340
392
  "hooks:repair",
341
393
  "hooks:watch",
342
394
  "sdm:catalog",
@@ -630,6 +682,14 @@ var REGISTRY = [
630
682
  migrated: true,
631
683
  description: "Desktop notification preferences (which events notify, sound on/off)."
632
684
  },
685
+ {
686
+ key: "rt.notify.eventBridges",
687
+ type: "array",
688
+ scopes: ["user"],
689
+ default: [],
690
+ merge: "replace",
691
+ description: "Event-bus glob rules that raise a desktop notification: [{pattern, category, title, message}]. pattern is matched against the events-bus topic (Bun.Glob semantics); title/message may interpolate `{field}` from the event payload. A fresh key, not an ownership-latch port, so a default is fine here."
692
+ },
633
693
  {
634
694
  key: "rt.cron",
635
695
  type: "object",
@@ -680,6 +740,15 @@ var REGISTRY = [
680
740
  migrated: true,
681
741
  description: "Home-repo snapshot daemon config: enabled, debounce/push delays, and the janitor threshold/interval for zones left dirty too long."
682
742
  },
743
+ {
744
+ key: "rt.teamSnapshot",
745
+ type: "object",
746
+ scopes: ["machine"],
747
+ default: { enabled: true, debounceSec: 20, pushDelaySec: 60, janitorThresholdHours: 6, janitorIntervalMin: 30, pullIntervalSec: 300 },
748
+ merge: "deep",
749
+ migrated: true,
750
+ description: "Team-clone snapshot daemon config: the home snapshot's fields plus pullIntervalSec, the fast-forward/rebase pull cadence for every clone under ~/.mattstack/teams."
751
+ },
683
752
  {
684
753
  key: "rt.sync",
685
754
  type: "object",
@@ -947,6 +1016,13 @@ var REGISTRY = [
947
1016
  merge: "replace",
948
1017
  description: "Days of MR inactivity before the board flags it stale, for this developer."
949
1018
  },
1019
+ {
1020
+ key: "board.gateGraceMinutes",
1021
+ type: "number",
1022
+ scopes: ["user"],
1023
+ merge: "replace",
1024
+ description: "Minutes an unanswered review gate stays open before the board parks the review (closes the pane, keeps the gate open). Default 90."
1025
+ },
950
1026
  {
951
1027
  key: "board.workspaces",
952
1028
  type: "object",
@@ -976,11 +1052,25 @@ var REGISTRY = [
976
1052
  description: "This developer's triage user-intent flags (which triage sweeps run automatically); a sibling flat key of board.triage.doctorSkill, not its container — the board reader assembles the two independently."
977
1053
  },
978
1054
  {
979
- key: "board.claudeCommand",
1055
+ key: "board.agent.account",
980
1056
  type: "string",
981
- scopes: ["machine"],
1057
+ scopes: ["user", "machine"],
1058
+ merge: "replace",
1059
+ description: "cswap account the board's review/respond/doctor panes launch under; unset uses the default claude profile. Replaces the retired board.claudeCommand."
1060
+ },
1061
+ {
1062
+ key: "board.agent.model",
1063
+ type: "string",
1064
+ scopes: ["user", "machine"],
1065
+ merge: "replace",
1066
+ description: "Default --model for the board's review/respond/doctor panes; unset omits the flag."
1067
+ },
1068
+ {
1069
+ key: "board.agent.effort",
1070
+ type: "string",
1071
+ scopes: ["user", "machine"],
982
1072
  merge: "replace",
983
- description: "Local command used to launch Claude Code for the board's review/respond/doctor panes."
1073
+ description: "Default --effort for the board's review/respond/doctor panes; unset omits the flag."
984
1074
  },
985
1075
  {
986
1076
  key: "board.cwds",
@@ -2002,6 +2092,15 @@ export {
2002
2092
  getSetting,
2003
2093
  getRun,
2004
2094
  getDef,
2095
+ gateWait,
2096
+ gateUnsubscribe,
2097
+ gateSubscriptions,
2098
+ gateSubscribe,
2099
+ gatePark,
2100
+ gateOpen,
2101
+ gateList,
2102
+ gateClose,
2103
+ gateAnswer,
2005
2104
  explainSetting,
2006
2105
  expandVariables,
2007
2106
  eventsWait,
@@ -2041,6 +2140,7 @@ export {
2041
2140
  abandonRun,
2042
2141
  SCOPE_ORDER,
2043
2142
  REGISTRY,
2143
+ GATE_BY_PANE,
2044
2144
  DEFAULT_WS_URL,
2045
2145
  DEFAULT_SOCK,
2046
2146
  COMMAND_NAMES
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mattstack/rt-client",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -18,7 +18,7 @@
18
18
  "./test/fake-daemon.ts": "./test/fake-daemon.ts"
19
19
  },
20
20
  "peerDependencies": {
21
- "@mattstack/glance": ">=0.23.0"
21
+ "@mattstack/glance": ">=0.24.0"
22
22
  },
23
23
  "dependencies": {
24
24
  "jsonc-parser": "^3.3.1"
package/src/client.ts CHANGED
@@ -442,3 +442,79 @@ export function paneFocus(
442
442
  ): Promise<RtResponse<Commands["pane:focus"]["data"]>> {
443
443
  return rtCommand<Commands["pane:focus"]["data"]>("pane:focus", { paneId: a.paneId }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
444
444
  }
445
+
446
+ // ─── Gates (BOARD-20/21 gate facility) ─────────────────────────────────────
447
+
448
+ export function gateOpen(
449
+ a: Commands["gate:open"]["payload"],
450
+ o: RtClientOptions = {},
451
+ ): Promise<RtResponse<Commands["gate:open"]["data"]>> {
452
+ const payload: Record<string, unknown> = { subject: a.subject, kind: a.kind, questions: a.questions };
453
+ for (const k of ["meta", "agent", "pane", "nudge"] as const) if (a[k] !== undefined) payload[k] = a[k];
454
+ return rtCommand<Commands["gate:open"]["data"]>("gate:open", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
455
+ }
456
+
457
+ export function gateAnswer(
458
+ a: Commands["gate:answer"]["payload"],
459
+ o: RtClientOptions = {},
460
+ ): Promise<RtResponse<Commands["gate:answer"]["data"]>> {
461
+ return rtCommand<Commands["gate:answer"]["data"]>("gate:answer", { id: a.id, answers: a.answers, by: a.by }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
462
+ }
463
+
464
+ /** Daemon clamps its own wait to 240s (gates-store.ts); the client abort
465
+ must outlive that cap, same +10s buffer as commands/events.ts's
466
+ IPC_TIMEOUT_MS over DAEMON_WAIT_MS. */
467
+ export function gateWait(
468
+ a: Commands["gate:wait"]["payload"],
469
+ o: RtClientOptions = {},
470
+ ): Promise<RtResponse<Commands["gate:wait"]["data"]>> {
471
+ const payload: Record<string, unknown> = { id: a.id };
472
+ if (a.waitMs !== undefined) payload.waitMs = a.waitMs;
473
+ return rtCommand<Commands["gate:wait"]["data"]>("gate:wait", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 250_000 });
474
+ }
475
+
476
+ export function gateList(
477
+ a: Commands["gate:list"]["payload"],
478
+ o: RtClientOptions = {},
479
+ ): Promise<RtResponse<Commands["gate:list"]["data"]>> {
480
+ const payload: Record<string, unknown> = {};
481
+ for (const k of ["open", "subjectPrefix", "kind", "limit", "cursor"] as const) if (a[k] !== undefined) payload[k] = a[k];
482
+ return rtCommand<Commands["gate:list"]["data"]>("gate:list", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
483
+ }
484
+
485
+ export function gatePark(
486
+ a: Commands["gate:park"]["payload"],
487
+ o: RtClientOptions = {},
488
+ ): Promise<RtResponse<Commands["gate:park"]["data"]>> {
489
+ return rtCommand<Commands["gate:park"]["data"]>("gate:park", { id: a.id }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
490
+ }
491
+
492
+ export function gateClose(
493
+ a: Commands["gate:close"]["payload"],
494
+ o: RtClientOptions = {},
495
+ ): Promise<RtResponse<Commands["gate:close"]["data"]>> {
496
+ return rtCommand<Commands["gate:close"]["data"]>("gate:close", { id: a.id, reason: a.reason }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
497
+ }
498
+
499
+ export function gateSubscribe(
500
+ a: Commands["gate:subscribe"]["payload"],
501
+ o: RtClientOptions = {},
502
+ ): Promise<RtResponse<Commands["gate:subscribe"]["data"]>> {
503
+ return rtCommand<Commands["gate:subscribe"]["data"]>("gate:subscribe", { subjectPrefix: a.subjectPrefix, session: a.session }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
504
+ }
505
+
506
+ export function gateUnsubscribe(
507
+ a: Commands["gate:unsubscribe"]["payload"],
508
+ o: RtClientOptions = {},
509
+ ): Promise<RtResponse<Commands["gate:unsubscribe"]["data"]>> {
510
+ return rtCommand<Commands["gate:unsubscribe"]["data"]>("gate:unsubscribe", { id: a.id }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
511
+ }
512
+
513
+ export function gateSubscriptions(
514
+ a: Commands["gate:subscriptions"]["payload"],
515
+ o: RtClientOptions = {},
516
+ ): Promise<RtResponse<Commands["gate:subscriptions"]["data"]>> {
517
+ const payload: Record<string, unknown> = {};
518
+ for (const k of ["session", "live"] as const) if (a[k] !== undefined) payload[k] = a[k];
519
+ return rtCommand<Commands["gate:subscriptions"]["data"]>("gate:subscriptions", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
520
+ }
package/src/commands.ts CHANGED
@@ -83,6 +83,41 @@ export interface ForgeTokenData {
83
83
  */
84
84
  export interface EventsBusEvent { id: number; topic: string; payload: unknown; emittedAt: number }
85
85
 
86
+ /**
87
+ * Duplicated shape on purpose, same reasoning as EventsBusEvent above:
88
+ * these mirror lib/daemon/gates-store.ts's types, which rt-client cannot
89
+ * import. gates-store.ts imports them back FROM this package (Commands
90
+ * already flows daemon -> rt-client, e.g. handlers/events.ts), so this is
91
+ * the single source of truth for the wire shape.
92
+ */
93
+ /** The `by` value a pane spells when it answers its own gate: the only
94
+ value gates-store.ts's release tracking (CAS winner or loser) reacts to. */
95
+ export const GATE_BY_PANE = "pane";
96
+
97
+ export type GateStatus = "open" | "answered" | "parked" | "closed";
98
+ export interface GateQuestion { id: string; label: string; multi: boolean; options: string[] }
99
+ export interface GateAnswer { answers: Record<string, string | string[] | { value: string | string[]; note?: string }>; by: string; answeredAt: number }
100
+ export interface GateRow {
101
+ id: string; subject: string; kind: string;
102
+ questions: GateQuestion[]; meta: Record<string, unknown> | null;
103
+ status: GateStatus; answer: GateAnswer | null;
104
+ openedAt: number; parkedAt: number | null; closedAt: number | null;
105
+ closedReason: "abandoned" | "superseded" | "pruned" | null;
106
+ agent: string | null; pane: string | null;
107
+ nudge: { session: string } | null;
108
+ delivery: { outcome: "delivered" | "dead-pane"; at: number } | null;
109
+ released: boolean;
110
+ }
111
+
112
+ export interface GateSubscription {
113
+ id: string;
114
+ subjectPrefix: string;
115
+ session: string;
116
+ createdAt: number;
117
+ lastDelivery: { outcome: "delivered" | "failed"; at: number } | null;
118
+ dead: boolean;
119
+ }
120
+
86
121
  /**
87
122
  * Duplicated shape on purpose, same reasoning as EventsBusEvent above:
88
123
  * these mirror lib/state/chat-store.ts's types, which rt-client cannot
@@ -538,6 +573,29 @@ export interface Commands {
538
573
  "repos:locate": { payload: { newPath: string; repo?: string; dryRun?: boolean }; data: unknown };
539
574
  "freshness:reconcile": { payload: Record<string, never>; data: unknown };
540
575
 
576
+ // ─── Gate facility (BOARD-20/21) ─────────────────────────────────────────
577
+ "gate:open": { payload: { subject: string; kind: string; questions: GateQuestion[]; meta?: Record<string, unknown>; agent?: string; pane?: string; nudge?: { session: string } }; data: { id: string; supersededId: string | null } };
578
+ /**
579
+ * A CAS loss is a DEFINED OUTCOME, not an error: `ok:true` with
580
+ * `conflict:true` and the WINNING row, so every consumer gets the winner
581
+ * typed with no envelope hacks. `ok:false` is reserved for
582
+ * not-found/closed/validation failures.
583
+ */
584
+ "gate:answer": { payload: { id: string; answers: GateAnswer["answers"]; by: string }; data: { row: GateRow; conflict?: true } };
585
+ /** `ok:false "not-found"` on an unknown id is terminal; the CLI loop must not re-enter on it.
586
+ * `timeout` carries no row (nothing settled); `answered`/`closed` always carry the settled row. */
587
+ "gate:wait": { payload: { id: string; waitMs?: number }; data: { status: "timeout" } | { status: "answered" | "closed"; row: GateRow } };
588
+ /** Paged like events:list: an omitted `limit` clamps daemon-side rather than
589
+ * forcing a full-table read; `cursor` is the paging rowid to resume from. */
590
+ "gate:list": { payload: { open?: boolean; subjectPrefix?: string; kind?: string; limit?: number; cursor?: number }; data: { gates: GateRow[]; cursor: number } };
591
+ "gate:park": { payload: { id: string }; data: { ok: true } };
592
+ "gate:close": { payload: { id: string; reason: "abandoned" | "superseded" | "pruned" }; data: { ok: true } };
593
+ "gate:subscribe": { payload: { subjectPrefix: string; session: string }; data: { id: string } };
594
+ "gate:unsubscribe": { payload: { id: string }; data: { removed: boolean } };
595
+ /** The shepherd's gap-recovery liveness check and the observability window
596
+ * onto delivery outcomes (dead marks included). */
597
+ "gate:subscriptions": { payload: { session?: string; live?: boolean }; data: { subscriptions: GateSubscription[] } };
598
+
541
599
  /** Wire reply on success is always `{ok:true, repaired}` (no `data`
542
600
  * wrapper) — `data` here documents the extra field the same way PingData
543
601
  * does for `ping`, not the literal wire nesting (R3). */
@@ -632,6 +690,15 @@ export const COMMAND_NAMES: readonly CommandName[] = [
632
690
  "endpoint:status",
633
691
  "repos:locate",
634
692
  "freshness:reconcile",
693
+ "gate:open",
694
+ "gate:answer",
695
+ "gate:wait",
696
+ "gate:list",
697
+ "gate:park",
698
+ "gate:close",
699
+ "gate:subscribe",
700
+ "gate:unsubscribe",
701
+ "gate:subscriptions",
635
702
  "hooks:repair",
636
703
  "hooks:watch",
637
704
  "sdm:catalog",
package/src/index.ts CHANGED
@@ -45,9 +45,18 @@ export {
45
45
  chatInvite,
46
46
  paneSend,
47
47
  paneFocus,
48
+ gateOpen,
49
+ gateAnswer,
50
+ gateWait,
51
+ gateList,
52
+ gatePark,
53
+ gateClose,
54
+ gateSubscribe,
55
+ gateUnsubscribe,
56
+ gateSubscriptions,
48
57
  } from "./client.ts";
49
58
 
50
- export { COMMAND_NAMES } from "./commands.ts";
59
+ export { COMMAND_NAMES, GATE_BY_PANE } from "./commands.ts";
51
60
  export type {
52
61
  Discussion,
53
62
  DemandDecl,
@@ -84,6 +93,11 @@ export type {
84
93
  PaneDelivery,
85
94
  PaneSendResult,
86
95
  PaneFocusResult,
96
+ GateStatus,
97
+ GateQuestion,
98
+ GateAnswer,
99
+ GateRow,
100
+ GateSubscription,
87
101
  } from "./commands.ts";
88
102
 
89
103
  export { subscribe, createRelay, DEFAULT_WS_URL } from "./relay.ts";
@@ -79,6 +79,14 @@ export const REGISTRY: readonly SettingDef[] = [
79
79
  migrated: true,
80
80
  description: "Desktop notification preferences (which events notify, sound on/off).",
81
81
  },
82
+ {
83
+ key: "rt.notify.eventBridges",
84
+ type: "array",
85
+ scopes: ["user"],
86
+ default: [],
87
+ merge: "replace",
88
+ description: "Event-bus glob rules that raise a desktop notification: [{pattern, category, title, message}]. pattern is matched against the events-bus topic (Bun.Glob semantics); title/message may interpolate `{field}` from the event payload. A fresh key, not an ownership-latch port, so a default is fine here.",
89
+ },
82
90
  {
83
91
  key: "rt.cron",
84
92
  type: "object",
@@ -129,6 +137,15 @@ export const REGISTRY: readonly SettingDef[] = [
129
137
  migrated: true,
130
138
  description: "Home-repo snapshot daemon config: enabled, debounce/push delays, and the janitor threshold/interval for zones left dirty too long.",
131
139
  },
140
+ {
141
+ key: "rt.teamSnapshot",
142
+ type: "object",
143
+ scopes: ["machine"],
144
+ default: { enabled: true, debounceSec: 20, pushDelaySec: 60, janitorThresholdHours: 6, janitorIntervalMin: 30, pullIntervalSec: 300 },
145
+ merge: "deep",
146
+ migrated: true,
147
+ description: "Team-clone snapshot daemon config: the home snapshot's fields plus pullIntervalSec, the fast-forward/rebase pull cadence for every clone under ~/.mattstack/teams.",
148
+ },
132
149
  {
133
150
  key: "rt.sync",
134
151
  type: "object",
@@ -421,6 +438,13 @@ export const REGISTRY: readonly SettingDef[] = [
421
438
  merge: "replace",
422
439
  description: "Days of MR inactivity before the board flags it stale, for this developer.",
423
440
  },
441
+ {
442
+ key: "board.gateGraceMinutes",
443
+ type: "number",
444
+ scopes: ["user"],
445
+ merge: "replace",
446
+ description: "Minutes an unanswered review gate stays open before the board parks the review (closes the pane, keeps the gate open). Default 90.",
447
+ },
424
448
  {
425
449
  key: "board.workspaces",
426
450
  type: "object",
@@ -452,11 +476,25 @@ export const REGISTRY: readonly SettingDef[] = [
452
476
 
453
477
  // --- board (machine) ---------------------------------------------------
454
478
  {
455
- key: "board.claudeCommand",
479
+ key: "board.agent.account",
456
480
  type: "string",
457
- scopes: ["machine"],
481
+ scopes: ["user", "machine"],
482
+ merge: "replace",
483
+ description: "cswap account the board's review/respond/doctor panes launch under; unset uses the default claude profile. Replaces the retired board.claudeCommand.",
484
+ },
485
+ {
486
+ key: "board.agent.model",
487
+ type: "string",
488
+ scopes: ["user", "machine"],
489
+ merge: "replace",
490
+ description: "Default --model for the board's review/respond/doctor panes; unset omits the flag.",
491
+ },
492
+ {
493
+ key: "board.agent.effort",
494
+ type: "string",
495
+ scopes: ["user", "machine"],
458
496
  merge: "replace",
459
- description: "Local command used to launch Claude Code for the board's review/respond/doctor panes.",
497
+ description: "Default --effort for the board's review/respond/doctor panes; unset omits the flag.",
460
498
  },
461
499
  {
462
500
  key: "board.cwds",