@mattstack/rt-client 0.14.0 → 0.15.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.
package/dist/client.d.ts CHANGED
@@ -105,10 +105,7 @@ export declare function chatWho(a: {
105
105
  }, o?: RtClientOptions): Promise<RtResponse<{
106
106
  members: ChatMember[];
107
107
  }>>;
108
- export declare function chatMark(a: {
109
- handle: string;
110
- room?: string;
111
- }, o?: RtClientOptions): Promise<RtResponse<Record<string, never>>>;
108
+ export declare function chatMark(a: Commands["chat:mark"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["chat:mark"]["data"]>>;
112
109
  export declare function chatMessages(a: {
113
110
  room: string;
114
111
  before?: number;
@@ -186,3 +183,15 @@ export declare function paneSend(a: Commands["pane:send"]["payload"], o?: RtClie
186
183
  /** Brings a herdr pane to the front. The daemon routes this to the tray, which
187
184
  owns the herdr focus and the native terminal-window raise. */
188
185
  export declare function paneFocus(a: Commands["pane:focus"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["pane:focus"]["data"]>>;
186
+ export declare function gateOpen(a: Commands["gate:open"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:open"]["data"]>>;
187
+ export declare function gateAnswer(a: Commands["gate:answer"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:answer"]["data"]>>;
188
+ /** Daemon clamps its own wait to 240s (gates-store.ts); the client abort
189
+ must outlive that cap, same +10s buffer as commands/events.ts's
190
+ IPC_TIMEOUT_MS over DAEMON_WAIT_MS. */
191
+ export declare function gateWait(a: Commands["gate:wait"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:wait"]["data"]>>;
192
+ export declare function gateList(a: Commands["gate:list"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:list"]["data"]>>;
193
+ export declare function gatePark(a: Commands["gate:park"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:park"]["data"]>>;
194
+ export declare function gateClose(a: Commands["gate:close"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:close"]["data"]>>;
195
+ export declare function gateSubscribe(a: Commands["gate:subscribe"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:subscribe"]["data"]>>;
196
+ export declare function gateUnsubscribe(a: Commands["gate:unsubscribe"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:unsubscribe"]["data"]>>;
197
+ 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
@@ -810,6 +869,7 @@ export interface Commands {
810
869
  payload: {
811
870
  handle: string;
812
871
  room?: string;
872
+ upto?: number;
813
873
  };
814
874
  data: Record<string, never>;
815
875
  };
@@ -1205,6 +1265,114 @@ export interface Commands {
1205
1265
  payload: Record<string, never>;
1206
1266
  data: unknown;
1207
1267
  };
1268
+ "gate:open": {
1269
+ payload: {
1270
+ subject: string;
1271
+ kind: string;
1272
+ questions: GateQuestion[];
1273
+ meta?: Record<string, unknown>;
1274
+ agent?: string;
1275
+ pane?: string;
1276
+ nudge?: {
1277
+ session: string;
1278
+ };
1279
+ };
1280
+ data: {
1281
+ id: string;
1282
+ supersededId: string | null;
1283
+ };
1284
+ };
1285
+ /**
1286
+ * A CAS loss is a DEFINED OUTCOME, not an error: `ok:true` with
1287
+ * `conflict:true` and the WINNING row, so every consumer gets the winner
1288
+ * typed with no envelope hacks. `ok:false` is reserved for
1289
+ * not-found/closed/validation failures.
1290
+ */
1291
+ "gate:answer": {
1292
+ payload: {
1293
+ id: string;
1294
+ answers: GateAnswer["answers"];
1295
+ by: string;
1296
+ };
1297
+ data: {
1298
+ row: GateRow;
1299
+ conflict?: true;
1300
+ };
1301
+ };
1302
+ /** `ok:false "not-found"` on an unknown id is terminal; the CLI loop must not re-enter on it.
1303
+ * `timeout` carries no row (nothing settled); `answered`/`closed` always carry the settled row. */
1304
+ "gate:wait": {
1305
+ payload: {
1306
+ id: string;
1307
+ waitMs?: number;
1308
+ };
1309
+ data: {
1310
+ status: "timeout";
1311
+ } | {
1312
+ status: "answered" | "closed";
1313
+ row: GateRow;
1314
+ };
1315
+ };
1316
+ /** Paged like events:list: an omitted `limit` clamps daemon-side rather than
1317
+ * forcing a full-table read; `cursor` is the paging rowid to resume from. */
1318
+ "gate:list": {
1319
+ payload: {
1320
+ open?: boolean;
1321
+ subjectPrefix?: string;
1322
+ kind?: string;
1323
+ limit?: number;
1324
+ cursor?: number;
1325
+ };
1326
+ data: {
1327
+ gates: GateRow[];
1328
+ cursor: number;
1329
+ };
1330
+ };
1331
+ "gate:park": {
1332
+ payload: {
1333
+ id: string;
1334
+ };
1335
+ data: {
1336
+ ok: true;
1337
+ };
1338
+ };
1339
+ "gate:close": {
1340
+ payload: {
1341
+ id: string;
1342
+ reason: "abandoned" | "superseded" | "pruned";
1343
+ };
1344
+ data: {
1345
+ ok: true;
1346
+ };
1347
+ };
1348
+ "gate:subscribe": {
1349
+ payload: {
1350
+ subjectPrefix: string;
1351
+ session: string;
1352
+ };
1353
+ data: {
1354
+ id: string;
1355
+ };
1356
+ };
1357
+ "gate:unsubscribe": {
1358
+ payload: {
1359
+ id: string;
1360
+ };
1361
+ data: {
1362
+ removed: boolean;
1363
+ };
1364
+ };
1365
+ /** The shepherd's gap-recovery liveness check and the observability window
1366
+ * onto delivery outcomes (dead marks included). */
1367
+ "gate:subscriptions": {
1368
+ payload: {
1369
+ session?: string;
1370
+ live?: boolean;
1371
+ };
1372
+ data: {
1373
+ subscriptions: GateSubscription[];
1374
+ };
1375
+ };
1208
1376
  /** Wire reply on success is always `{ok:true, repaired}` (no `data`
1209
1377
  * wrapper) — `data` here documents the extra field the same way PingData
1210
1378
  * 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
@@ -118,6 +118,8 @@ function chatMark(a, o = {}) {
118
118
  const payload = { handle: a.handle };
119
119
  if (a.room !== undefined)
120
120
  payload.room = a.room;
121
+ if (a.upto !== undefined)
122
+ payload.upto = a.upto;
121
123
  return rtCommand("chat:mark", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
122
124
  }
123
125
  function chatMessages(a, o = {}) {
@@ -268,7 +270,50 @@ function paneSend(a, o = {}) {
268
270
  function paneFocus(a, o = {}) {
269
271
  return rtCommand("pane:focus", { paneId: a.paneId }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
270
272
  }
273
+ function gateOpen(a, o = {}) {
274
+ const payload = { subject: a.subject, kind: a.kind, questions: a.questions };
275
+ for (const k of ["meta", "agent", "pane", "nudge"])
276
+ if (a[k] !== undefined)
277
+ payload[k] = a[k];
278
+ return rtCommand("gate:open", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
279
+ }
280
+ function gateAnswer(a, o = {}) {
281
+ return rtCommand("gate:answer", { id: a.id, answers: a.answers, by: a.by }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
282
+ }
283
+ function gateWait(a, o = {}) {
284
+ const payload = { id: a.id };
285
+ if (a.waitMs !== undefined)
286
+ payload.waitMs = a.waitMs;
287
+ return rtCommand("gate:wait", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 250000 });
288
+ }
289
+ function gateList(a, o = {}) {
290
+ const payload = {};
291
+ for (const k of ["open", "subjectPrefix", "kind", "limit", "cursor"])
292
+ if (a[k] !== undefined)
293
+ payload[k] = a[k];
294
+ return rtCommand("gate:list", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
295
+ }
296
+ function gatePark(a, o = {}) {
297
+ return rtCommand("gate:park", { id: a.id }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
298
+ }
299
+ function gateClose(a, o = {}) {
300
+ return rtCommand("gate:close", { id: a.id, reason: a.reason }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
301
+ }
302
+ function gateSubscribe(a, o = {}) {
303
+ return rtCommand("gate:subscribe", { subjectPrefix: a.subjectPrefix, session: a.session }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
304
+ }
305
+ function gateUnsubscribe(a, o = {}) {
306
+ return rtCommand("gate:unsubscribe", { id: a.id }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
307
+ }
308
+ function gateSubscriptions(a, o = {}) {
309
+ const payload = {};
310
+ for (const k of ["session", "live"])
311
+ if (a[k] !== undefined)
312
+ payload[k] = a[k];
313
+ return rtCommand("gate:subscriptions", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
314
+ }
271
315
  // src/commands.ts
316
+ var GATE_BY_PANE = "pane";
272
317
  var COMMAND_NAMES = [
273
318
  "project-mrs:read",
274
319
  "discussions:read",
@@ -337,6 +382,15 @@ var COMMAND_NAMES = [
337
382
  "endpoint:status",
338
383
  "repos:locate",
339
384
  "freshness:reconcile",
385
+ "gate:open",
386
+ "gate:answer",
387
+ "gate:wait",
388
+ "gate:list",
389
+ "gate:park",
390
+ "gate:close",
391
+ "gate:subscribe",
392
+ "gate:unsubscribe",
393
+ "gate:subscriptions",
340
394
  "hooks:repair",
341
395
  "hooks:watch",
342
396
  "sdm:catalog",
@@ -688,6 +742,15 @@ var REGISTRY = [
688
742
  migrated: true,
689
743
  description: "Home-repo snapshot daemon config: enabled, debounce/push delays, and the janitor threshold/interval for zones left dirty too long."
690
744
  },
745
+ {
746
+ key: "rt.teamSnapshot",
747
+ type: "object",
748
+ scopes: ["machine"],
749
+ default: { enabled: true, debounceSec: 20, pushDelaySec: 60, janitorThresholdHours: 6, janitorIntervalMin: 30, pullIntervalSec: 300 },
750
+ merge: "deep",
751
+ migrated: true,
752
+ 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."
753
+ },
691
754
  {
692
755
  key: "rt.sync",
693
756
  type: "object",
@@ -990,6 +1053,14 @@ var REGISTRY = [
990
1053
  merge: "deep",
991
1054
  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."
992
1055
  },
1056
+ {
1057
+ key: "board.reReview",
1058
+ type: "object",
1059
+ scopes: ["user", "team"],
1060
+ merge: "deep",
1061
+ default: { enabled: true },
1062
+ description: "Gate for the board's automatic re-review sweep ({enabled}); rt's cron.triage step installs the board-triage trigger only while this is on. A fresh key, not an ownership-latch port, so a default is fine here."
1063
+ },
993
1064
  {
994
1065
  key: "board.agent.account",
995
1066
  type: "string",
@@ -2031,6 +2102,15 @@ export {
2031
2102
  getSetting,
2032
2103
  getRun,
2033
2104
  getDef,
2105
+ gateWait,
2106
+ gateUnsubscribe,
2107
+ gateSubscriptions,
2108
+ gateSubscribe,
2109
+ gatePark,
2110
+ gateOpen,
2111
+ gateList,
2112
+ gateClose,
2113
+ gateAnswer,
2034
2114
  explainSetting,
2035
2115
  expandVariables,
2036
2116
  eventsWait,
@@ -2070,6 +2150,7 @@ export {
2070
2150
  abandonRun,
2071
2151
  SCOPE_ORDER,
2072
2152
  REGISTRY,
2153
+ GATE_BY_PANE,
2073
2154
  DEFAULT_WS_URL,
2074
2155
  DEFAULT_SOCK,
2075
2156
  COMMAND_NAMES
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mattstack/rt-client",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
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
@@ -223,12 +223,13 @@ export function chatWho(
223
223
  }
224
224
 
225
225
  export function chatMark(
226
- a: { handle: string; room?: string },
226
+ a: Commands["chat:mark"]["payload"],
227
227
  o: RtClientOptions = {},
228
- ): Promise<RtResponse<Record<string, never>>> {
228
+ ): Promise<RtResponse<Commands["chat:mark"]["data"]>> {
229
229
  const payload: Record<string, unknown> = { handle: a.handle };
230
230
  if (a.room !== undefined) payload.room = a.room;
231
- return rtCommand<Record<string, never>>("chat:mark", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
231
+ if (a.upto !== undefined) payload.upto = a.upto;
232
+ return rtCommand<Commands["chat:mark"]["data"]>("chat:mark", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
232
233
  }
233
234
 
234
235
  export function chatMessages(
@@ -442,3 +443,79 @@ export function paneFocus(
442
443
  ): Promise<RtResponse<Commands["pane:focus"]["data"]>> {
443
444
  return rtCommand<Commands["pane:focus"]["data"]>("pane:focus", { paneId: a.paneId }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
444
445
  }
446
+
447
+ // ─── Gates (BOARD-20/21 gate facility) ─────────────────────────────────────
448
+
449
+ export function gateOpen(
450
+ a: Commands["gate:open"]["payload"],
451
+ o: RtClientOptions = {},
452
+ ): Promise<RtResponse<Commands["gate:open"]["data"]>> {
453
+ const payload: Record<string, unknown> = { subject: a.subject, kind: a.kind, questions: a.questions };
454
+ for (const k of ["meta", "agent", "pane", "nudge"] as const) if (a[k] !== undefined) payload[k] = a[k];
455
+ return rtCommand<Commands["gate:open"]["data"]>("gate:open", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
456
+ }
457
+
458
+ export function gateAnswer(
459
+ a: Commands["gate:answer"]["payload"],
460
+ o: RtClientOptions = {},
461
+ ): Promise<RtResponse<Commands["gate:answer"]["data"]>> {
462
+ 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 });
463
+ }
464
+
465
+ /** Daemon clamps its own wait to 240s (gates-store.ts); the client abort
466
+ must outlive that cap, same +10s buffer as commands/events.ts's
467
+ IPC_TIMEOUT_MS over DAEMON_WAIT_MS. */
468
+ export function gateWait(
469
+ a: Commands["gate:wait"]["payload"],
470
+ o: RtClientOptions = {},
471
+ ): Promise<RtResponse<Commands["gate:wait"]["data"]>> {
472
+ const payload: Record<string, unknown> = { id: a.id };
473
+ if (a.waitMs !== undefined) payload.waitMs = a.waitMs;
474
+ return rtCommand<Commands["gate:wait"]["data"]>("gate:wait", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 250_000 });
475
+ }
476
+
477
+ export function gateList(
478
+ a: Commands["gate:list"]["payload"],
479
+ o: RtClientOptions = {},
480
+ ): Promise<RtResponse<Commands["gate:list"]["data"]>> {
481
+ const payload: Record<string, unknown> = {};
482
+ for (const k of ["open", "subjectPrefix", "kind", "limit", "cursor"] as const) if (a[k] !== undefined) payload[k] = a[k];
483
+ return rtCommand<Commands["gate:list"]["data"]>("gate:list", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
484
+ }
485
+
486
+ export function gatePark(
487
+ a: Commands["gate:park"]["payload"],
488
+ o: RtClientOptions = {},
489
+ ): Promise<RtResponse<Commands["gate:park"]["data"]>> {
490
+ return rtCommand<Commands["gate:park"]["data"]>("gate:park", { id: a.id }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
491
+ }
492
+
493
+ export function gateClose(
494
+ a: Commands["gate:close"]["payload"],
495
+ o: RtClientOptions = {},
496
+ ): Promise<RtResponse<Commands["gate:close"]["data"]>> {
497
+ return rtCommand<Commands["gate:close"]["data"]>("gate:close", { id: a.id, reason: a.reason }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
498
+ }
499
+
500
+ export function gateSubscribe(
501
+ a: Commands["gate:subscribe"]["payload"],
502
+ o: RtClientOptions = {},
503
+ ): Promise<RtResponse<Commands["gate:subscribe"]["data"]>> {
504
+ return rtCommand<Commands["gate:subscribe"]["data"]>("gate:subscribe", { subjectPrefix: a.subjectPrefix, session: a.session }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
505
+ }
506
+
507
+ export function gateUnsubscribe(
508
+ a: Commands["gate:unsubscribe"]["payload"],
509
+ o: RtClientOptions = {},
510
+ ): Promise<RtResponse<Commands["gate:unsubscribe"]["data"]>> {
511
+ return rtCommand<Commands["gate:unsubscribe"]["data"]>("gate:unsubscribe", { id: a.id }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
512
+ }
513
+
514
+ export function gateSubscriptions(
515
+ a: Commands["gate:subscriptions"]["payload"],
516
+ o: RtClientOptions = {},
517
+ ): Promise<RtResponse<Commands["gate:subscriptions"]["data"]>> {
518
+ const payload: Record<string, unknown> = {};
519
+ for (const k of ["session", "live"] as const) if (a[k] !== undefined) payload[k] = a[k];
520
+ return rtCommand<Commands["gate:subscriptions"]["data"]>("gate:subscriptions", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
521
+ }
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
@@ -437,7 +472,7 @@ export interface Commands {
437
472
  "chat:read": { payload: { handle: string; room?: string; limit?: number; sinceMs?: number }; data: { rooms: { room: string; messages: ChatMessage[] }[] } };
438
473
  "chat:rooms": { payload: { handle: string; includeArchived?: boolean }; data: { rooms: RoomSummary[] } };
439
474
  "chat:who": { payload: { room: string }; data: { members: ChatMember[] } };
440
- "chat:mark": { payload: { handle: string; room?: string }; data: Record<string, never> };
475
+ "chat:mark": { payload: { handle: string; room?: string; upto?: number }; data: Record<string, never> };
441
476
  "chat:messages": { payload: { room: string; before?: number; limit?: number }; data: { messages: ChatMessage[] } };
442
477
 
443
478
  // A session id keys these to one signed-in handle, not a room-membership
@@ -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";
@@ -137,6 +137,15 @@ export const REGISTRY: readonly SettingDef[] = [
137
137
  migrated: true,
138
138
  description: "Home-repo snapshot daemon config: enabled, debounce/push delays, and the janitor threshold/interval for zones left dirty too long.",
139
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
+ },
140
149
  {
141
150
  key: "rt.sync",
142
151
  type: "object",
@@ -464,6 +473,14 @@ export const REGISTRY: readonly SettingDef[] = [
464
473
  merge: "deep",
465
474
  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.",
466
475
  },
476
+ {
477
+ key: "board.reReview",
478
+ type: "object",
479
+ scopes: ["user", "team"],
480
+ merge: "deep",
481
+ default: { enabled: true },
482
+ description: "Gate for the board's automatic re-review sweep ({enabled}); rt's cron.triage step installs the board-triage trigger only while this is on. A fresh key, not an ownership-latch port, so a default is fine here.",
483
+ },
467
484
 
468
485
  // --- board (machine) ---------------------------------------------------
469
486
  {