@mattstack/rt-client 0.14.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",
@@ -688,6 +740,15 @@ var REGISTRY = [
688
740
  migrated: true,
689
741
  description: "Home-repo snapshot daemon config: enabled, debounce/push delays, and the janitor threshold/interval for zones left dirty too long."
690
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
+ },
691
752
  {
692
753
  key: "rt.sync",
693
754
  type: "object",
@@ -2031,6 +2092,15 @@ export {
2031
2092
  getSetting,
2032
2093
  getRun,
2033
2094
  getDef,
2095
+ gateWait,
2096
+ gateUnsubscribe,
2097
+ gateSubscriptions,
2098
+ gateSubscribe,
2099
+ gatePark,
2100
+ gateOpen,
2101
+ gateList,
2102
+ gateClose,
2103
+ gateAnswer,
2034
2104
  explainSetting,
2035
2105
  expandVariables,
2036
2106
  eventsWait,
@@ -2070,6 +2140,7 @@ export {
2070
2140
  abandonRun,
2071
2141
  SCOPE_ORDER,
2072
2142
  REGISTRY,
2143
+ GATE_BY_PANE,
2073
2144
  DEFAULT_WS_URL,
2074
2145
  DEFAULT_SOCK,
2075
2146
  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.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";
@@ -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",