@mattstack/rt-client 0.16.0 → 0.19.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
@@ -195,3 +195,33 @@ export declare function gateClose(a: Commands["gate:close"]["payload"], o?: RtCl
195
195
  export declare function gateSubscribe(a: Commands["gate:subscribe"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:subscribe"]["data"]>>;
196
196
  export declare function gateUnsubscribe(a: Commands["gate:unsubscribe"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:unsubscribe"]["data"]>>;
197
197
  export declare function gateSubscriptions(a: Commands["gate:subscriptions"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:subscriptions"]["data"]>>;
198
+ /** Provisions the room, workspace and subscription before it answers, so it
199
+ gets the same 60s budget as a spawn rather than the 10s default. */
200
+ export declare function herdStart(a: Commands["herd:start"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["herd:start"]["data"]>>;
201
+ /** Worktree provision plus agent launch; the 10s default cannot cover it. */
202
+ export declare function herdSpawn(a: Commands["herd:spawn"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["herd:spawn"]["data"]>>;
203
+ export declare function herdAsk(a: Commands["herd:ask"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["herd:ask"]["data"]>>;
204
+ export declare function herdMilestone(a: Commands["herd:milestone"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["herd:milestone"]["data"]>>;
205
+ export declare function herdAnswer(a: Commands["herd:answer"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["herd:answer"]["data"]>>;
206
+ /** A disposable job's report also closes its pane, one herdr CLI call under the runner's own 15s budget. */
207
+ export declare function herdReport(a: Commands["herd:report"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["herd:report"]["data"]>>;
208
+ export declare function herdGates(a: Commands["herd:gates"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["herd:gates"]["data"]>>;
209
+ export declare function herdStatus(a: Commands["herd:status"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["herd:status"]["data"]>>;
210
+ export declare function herdList(a?: Commands["herd:list"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["herd:list"]["data"]>>;
211
+ export declare function herdResume(a: Commands["herd:resume"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["herd:resume"]["data"]>>;
212
+ /** Closes a herdr pane, one CLI call under the runner's own 15s budget. */
213
+ export declare function herdClose(a: Commands["herd:close"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["herd:close"]["data"]>>;
214
+ /** Three sequential herdr CLI calls (pane get, tab create, pane run), each under the runner's own 15s budget. */
215
+ export declare function herdAttend(a: Commands["herd:attend"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["herd:attend"]["data"]>>;
216
+ /** Closes panes, disposes worktrees and archives the room in one pass; the
217
+ worktree disposals alone can outrun a spawn's budget. */
218
+ export declare function herdWrapUp(a: Commands["herd:wrap-up"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["herd:wrap-up"]["data"]>>;
219
+ /** Runs `herdr session stop`, one CLI call under the runner's own 15s budget. */
220
+ export declare function herdStopHidden(_a: Commands["herd:stop-hidden"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["herd:stop-hidden"]["data"]>>;
221
+ /** May spawn `herdr server` and wait for it to bind; budget matches bg-service's own 10s readyTimeoutMs plus margin. */
222
+ export declare function bgEnsure(a?: Commands["bg:ensure"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["bg:ensure"]["data"]>>;
223
+ /** Never ensures/spawns; a plain read of the current state. */
224
+ export declare function bgStatus(o?: RtClientOptions): Promise<RtResponse<Commands["bg:status"]["data"]>>;
225
+ /** Asks the daemon to stop the background server; refuses (ok:false) while any claim is live, naming the owners. */
226
+ export declare function bgStop(o?: RtClientOptions): Promise<RtResponse<Commands["bg:stop"]["data"]>>;
227
+ export declare function bgRelease(a: Commands["bg:release"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["bg:release"]["data"]>>;
@@ -165,6 +165,59 @@ export interface GateSubscription {
165
165
  } | null;
166
166
  dead: boolean;
167
167
  }
168
+ export interface HerdInfo {
169
+ id: string;
170
+ repo: string;
171
+ room: string;
172
+ workspace: string;
173
+ shepherdSession: string;
174
+ shepherdHandle: string;
175
+ herdrSocket: string | null;
176
+ hidden: boolean;
177
+ status: "active" | "wrapped";
178
+ createdAt: number;
179
+ wrappedAt: number | null;
180
+ }
181
+ /** A herd row as `herd:list` reports it: the registry row plus how many jobs hang off it. */
182
+ export interface HerdListRow extends HerdInfo {
183
+ jobs: number;
184
+ }
185
+ export interface HerdJobInfo {
186
+ herd: string;
187
+ name: string;
188
+ worktree: string;
189
+ branch: string | null;
190
+ tree: string | null;
191
+ pane: string | null;
192
+ agentSession: string | null;
193
+ agentId: string | null;
194
+ handle: string;
195
+ status: "spawning" | "active" | "at-gate" | "at-milestone" | "done" | "closed" | "crashed";
196
+ disposable: boolean;
197
+ lastGate: string | null;
198
+ lastReport: number | null;
199
+ createdAt: number;
200
+ updatedAt: number;
201
+ }
202
+ /** `lastGateStatus`/`lastGateDelivery` come from the job's `lastGate` row: an `answered` gate whose delivery is `dead-pane` is the "answered, worker not woken" case the shepherd must act on. */
203
+ export interface HerdStatusData {
204
+ herd: HerdInfo;
205
+ jobs: Array<HerdJobInfo & {
206
+ openGate: string | null;
207
+ paneStatus: string | null;
208
+ lastGateStatus: GateStatus | null;
209
+ lastGateDelivery: "delivered" | "dead-pane" | null;
210
+ }>;
211
+ unread: number;
212
+ lifecycleConnected: boolean;
213
+ hiddenUp: boolean | null;
214
+ /** The shepherd session's own `herd:<id>/` subscription row, or null when none is live. */
215
+ subscription: {
216
+ id: string;
217
+ dead: boolean;
218
+ lastDelivery: GateSubscription["lastDelivery"];
219
+ } | null;
220
+ }
168
221
  /**
169
222
  * Duplicated shape on purpose, same reasoning as EventsBusEvent above:
170
223
  * these mirror lib/state/chat-store.ts's types, which rt-client cannot
@@ -285,9 +338,13 @@ export interface PaneSendResult {
285
338
  delivered: PaneDelivery;
286
339
  reason?: string;
287
340
  }
341
+ /** `attendTab` is set only for a `bg:` ref: focus for a background pane IS
342
+ the attend flow (a visible tab running a terminal attach), and this is
343
+ that tab's id. */
288
344
  export interface PaneFocusResult {
289
345
  paneId: string;
290
346
  focused: boolean;
347
+ attendTab?: string;
291
348
  }
292
349
  export type Attention = {
293
350
  needs: boolean;
@@ -1019,6 +1076,10 @@ export interface Commands {
1019
1076
  workspace?: string;
1020
1077
  tab?: string;
1021
1078
  extraArgs?: string;
1079
+ env?: Record<string, string>;
1080
+ herdrSocket?: string;
1081
+ handle?: string;
1082
+ bg?: boolean;
1022
1083
  };
1023
1084
  data: AgentRecord;
1024
1085
  };
@@ -1108,9 +1169,12 @@ export interface Commands {
1108
1169
  };
1109
1170
  data: PaneSendResult;
1110
1171
  };
1172
+ /** `callerWorkspace` (HERDR_WORKSPACE_ID) is required only for a `bg:`
1173
+ ref, whose focus opens an attend tab in the caller's own workspace. */
1111
1174
  "pane:focus": {
1112
1175
  payload: {
1113
1176
  paneId: string;
1177
+ callerWorkspace?: string;
1114
1178
  };
1115
1179
  data: PaneFocusResult;
1116
1180
  };
@@ -1390,6 +1454,179 @@ export interface Commands {
1390
1454
  subscriptions: GateSubscription[];
1391
1455
  };
1392
1456
  };
1457
+ "herd:start": {
1458
+ payload: {
1459
+ name: string;
1460
+ repo: string;
1461
+ session: string;
1462
+ hidden?: boolean;
1463
+ };
1464
+ data: {
1465
+ herd: string;
1466
+ room: string;
1467
+ workspace: string;
1468
+ subscription: string;
1469
+ handle: string;
1470
+ hidden: boolean;
1471
+ };
1472
+ };
1473
+ "herd:resume": {
1474
+ payload: {
1475
+ herd: string;
1476
+ session: string;
1477
+ };
1478
+ data: {
1479
+ subscription: string;
1480
+ gates: GateRow[];
1481
+ unread: number;
1482
+ status: HerdStatusData;
1483
+ handle: string;
1484
+ };
1485
+ };
1486
+ "herd:status": {
1487
+ payload: {
1488
+ herd: string;
1489
+ };
1490
+ data: HerdStatusData;
1491
+ };
1492
+ /** Active herds only unless `all`, so a shepherd's "which herd am I on" question has one answer. */
1493
+ "herd:list": {
1494
+ payload: {
1495
+ all?: boolean;
1496
+ };
1497
+ data: {
1498
+ herds: HerdListRow[];
1499
+ };
1500
+ };
1501
+ "herd:close": {
1502
+ payload: {
1503
+ herd: string;
1504
+ job: string;
1505
+ };
1506
+ data: {
1507
+ job: string;
1508
+ status: "closed";
1509
+ };
1510
+ };
1511
+ /** `brief` is the brief TEXT, not a path: the CLI reads the file. It is stored at `<jobsRoot>/<herd>/<job>/job.md`, so a respawn with `dir` and no `brief` reads it back. */
1512
+ "herd:spawn": {
1513
+ payload: {
1514
+ herd: string;
1515
+ job: string;
1516
+ brief?: string;
1517
+ dir?: string;
1518
+ model?: string;
1519
+ effort?: string;
1520
+ account?: string;
1521
+ disposable?: boolean;
1522
+ };
1523
+ data: {
1524
+ herd: string;
1525
+ job: string;
1526
+ pane: string;
1527
+ worktree: string;
1528
+ branch: string | null;
1529
+ tree: string | null; /** null = no provisioning ran (--dir); false = cold create, worth announcing. */
1530
+ wasOnDeck: boolean | null;
1531
+ agentId: string;
1532
+ sessionId: string;
1533
+ handle: string;
1534
+ };
1535
+ };
1536
+ "herd:gates": {
1537
+ payload: {
1538
+ herd: string;
1539
+ };
1540
+ data: {
1541
+ gates: GateRow[];
1542
+ };
1543
+ };
1544
+ "herd:ask": {
1545
+ payload: {
1546
+ herd: string;
1547
+ job: string;
1548
+ session: string;
1549
+ pane?: string;
1550
+ questions: GateQuestion[];
1551
+ context?: string;
1552
+ };
1553
+ data: {
1554
+ gate: string;
1555
+ };
1556
+ };
1557
+ "herd:milestone": {
1558
+ payload: {
1559
+ herd: string;
1560
+ job: string;
1561
+ session: string;
1562
+ pane?: string;
1563
+ artifact: string;
1564
+ summary?: string;
1565
+ };
1566
+ data: {
1567
+ gate: string;
1568
+ message: number;
1569
+ };
1570
+ };
1571
+ "herd:answer": {
1572
+ payload: {
1573
+ gate: string;
1574
+ };
1575
+ data: {
1576
+ gate: string;
1577
+ status: GateStatus;
1578
+ answer: GateAnswer | null;
1579
+ closedReason: GateRow["closedReason"];
1580
+ };
1581
+ };
1582
+ "herd:report": {
1583
+ payload: {
1584
+ herd: string;
1585
+ job: string;
1586
+ body: string;
1587
+ };
1588
+ data: {
1589
+ message: number;
1590
+ };
1591
+ };
1592
+ /** `callerWorkspace` is the attending session's own HERDR_WORKSPACE_ID: the attached tab opens there, not in the herd's workspace. */
1593
+ "herd:attend": {
1594
+ payload: {
1595
+ herd: string;
1596
+ job: string;
1597
+ callerWorkspace: string;
1598
+ };
1599
+ data: {
1600
+ tab: string;
1601
+ pane: string;
1602
+ };
1603
+ };
1604
+ "herd:stop-hidden": {
1605
+ payload: Record<string, never>;
1606
+ data: {
1607
+ stopped: boolean;
1608
+ };
1609
+ };
1610
+ "herd:wrap-up": {
1611
+ payload: {
1612
+ herd: string;
1613
+ closePanes?: boolean;
1614
+ dispose?: string[];
1615
+ deleteJobDirs?: boolean;
1616
+ archiveRoom?: boolean;
1617
+ };
1618
+ data: {
1619
+ closed: string[];
1620
+ workspaceClosed: boolean;
1621
+ disposed: string[];
1622
+ refused: Array<{
1623
+ tree: string;
1624
+ reason: string;
1625
+ }>;
1626
+ deletedJobDirs: boolean;
1627
+ archived: boolean;
1628
+ };
1629
+ };
1393
1630
  /** Wire reply on success is always `{ok:true, repaired}` (no `data`
1394
1631
  * wrapper) — `data` here documents the extra field the same way PingData
1395
1632
  * does for `ping`, not the literal wire nesting (R3). */
@@ -1488,6 +1725,46 @@ export interface Commands {
1488
1725
  };
1489
1726
  data: WorktreeAdoptData;
1490
1727
  };
1728
+ "bg:ensure": {
1729
+ payload: {
1730
+ claim?: string;
1731
+ };
1732
+ data: {
1733
+ socket: string;
1734
+ started: boolean;
1735
+ parity: {
1736
+ ok: boolean;
1737
+ drift: string[];
1738
+ } | null;
1739
+ };
1740
+ };
1741
+ "bg:status": {
1742
+ payload: Record<string, never>;
1743
+ data: {
1744
+ up: boolean;
1745
+ socket: string;
1746
+ claims: Array<{
1747
+ owner: string;
1748
+ pane: string | null;
1749
+ createdAt: number;
1750
+ }>;
1751
+ };
1752
+ };
1753
+ /** Rejects (`ok:false`) naming every live claim owner while any claim is held. */
1754
+ "bg:stop": {
1755
+ payload: Record<string, never>;
1756
+ data: {
1757
+ stopped: boolean;
1758
+ };
1759
+ };
1760
+ "bg:release": {
1761
+ payload: {
1762
+ claim: string;
1763
+ };
1764
+ data: {
1765
+ released: boolean;
1766
+ };
1767
+ };
1491
1768
  }
1492
1769
  export type CommandName = keyof Commands;
1493
1770
  export declare const COMMAND_NAMES: readonly CommandName[];
package/dist/index.d.ts CHANGED
@@ -1,14 +1,16 @@
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, gateOpen, gateAnswer, gateWait, gateList, gatePark, gateClose, gateSubscribe, gateUnsubscribe, gateSubscriptions, } from "./client.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, herdStart, herdSpawn, herdAsk, herdMilestone, herdAnswer, herdReport, herdGates, herdStatus, herdList, herdResume, herdClose, herdAttend, herdWrapUp, herdStopHidden, bgEnsure, bgStatus, bgStop, bgRelease, } from "./client.ts";
4
4
  export { COMMAND_NAMES, GATE_BY_PANE, gateOptionValue, gateOptionLabel } 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, GateOption, GateOrigin, GateQuestion, GateAnswer, GateRow, GateSubscription, } 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, GateOption, GateOrigin, GateQuestion, GateAnswer, GateRow, GateSubscription, HerdInfo, HerdListRow, HerdJobInfo, HerdStatusData, } 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";
9
9
  export { repoNameForPath } from "./repos.ts";
10
10
  export { decidePlacement, openSmartPane } from "./smart-pane.ts";
11
11
  export type { Placement, PlacementOpts, HerdrCall } from "./smart-pane.ts";
12
+ export { BG_PREFIX, parsePaneRef, formatPaneRef } from "./pane-ref.ts";
13
+ export type { PaneServer, PaneRef } from "./pane-ref.ts";
12
14
  export { getSetting, listSettings, explainSetting, expandVariables, SCOPE_ORDER, setSettingsWarnSink } from "./settings/resolve.ts";
13
15
  export type { Scope, Provenance, ResolveOpts, Resolved, InvalidScope, ListedSetting, ExplainRow, ExpandCtx, } from "./settings/resolve.ts";
14
16
  export { setSetting, unsetSetting } from "./settings/write.ts";
package/dist/index.js CHANGED
@@ -205,7 +205,7 @@ function eventsList(payload, o = {}) {
205
205
  }
206
206
  function agentStart(a, o = {}) {
207
207
  const payload = { repo: a.repo, cwd: a.cwd };
208
- for (const k of ["prompt", "surface", "model", "effort", "account", "label", "caller", "workspace", "tab", "extraArgs"]) {
208
+ for (const k of ["prompt", "surface", "model", "effort", "account", "label", "caller", "workspace", "tab", "extraArgs", "env", "herdrSocket", "handle", "bg"]) {
209
209
  if (a[k] !== undefined)
210
210
  payload[k] = a[k];
211
211
  }
@@ -268,7 +268,10 @@ function paneSend(a, o = {}) {
268
268
  return rtCommand("pane:send", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30000 });
269
269
  }
270
270
  function paneFocus(a, o = {}) {
271
- return rtCommand("pane:focus", { paneId: a.paneId }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
271
+ const payload = { paneId: a.paneId };
272
+ if (a.callerWorkspace !== undefined)
273
+ payload.callerWorkspace = a.callerWorkspace;
274
+ return rtCommand("pane:focus", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
272
275
  }
273
276
  function gateOpen(a, o = {}) {
274
277
  const payload = { subject: a.subject, kind: a.kind, questions: a.questions };
@@ -312,6 +315,85 @@ function gateSubscriptions(a, o = {}) {
312
315
  payload[k] = a[k];
313
316
  return rtCommand("gate:subscriptions", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
314
317
  }
318
+ function herdStart(a, o = {}) {
319
+ const payload = { name: a.name, repo: a.repo, session: a.session };
320
+ if (a.hidden !== undefined)
321
+ payload.hidden = a.hidden;
322
+ return rtCommand("herd:start", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 60000 });
323
+ }
324
+ function herdSpawn(a, o = {}) {
325
+ const payload = { herd: a.herd, job: a.job };
326
+ for (const k of ["brief", "dir", "model", "effort", "account", "disposable"])
327
+ if (a[k] !== undefined)
328
+ payload[k] = a[k];
329
+ return rtCommand("herd:spawn", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 60000 });
330
+ }
331
+ function herdAsk(a, o = {}) {
332
+ const payload = { herd: a.herd, job: a.job, session: a.session, questions: a.questions };
333
+ for (const k of ["pane", "context"])
334
+ if (a[k] !== undefined)
335
+ payload[k] = a[k];
336
+ return rtCommand("herd:ask", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
337
+ }
338
+ function herdMilestone(a, o = {}) {
339
+ const payload = { herd: a.herd, job: a.job, session: a.session, artifact: a.artifact };
340
+ for (const k of ["pane", "summary"])
341
+ if (a[k] !== undefined)
342
+ payload[k] = a[k];
343
+ return rtCommand("herd:milestone", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
344
+ }
345
+ function herdAnswer(a, o = {}) {
346
+ return rtCommand("herd:answer", { gate: a.gate }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
347
+ }
348
+ function herdReport(a, o = {}) {
349
+ return rtCommand("herd:report", { herd: a.herd, job: a.job, body: a.body }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30000 });
350
+ }
351
+ function herdGates(a, o = {}) {
352
+ return rtCommand("herd:gates", { herd: a.herd }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
353
+ }
354
+ function herdStatus(a, o = {}) {
355
+ return rtCommand("herd:status", { herd: a.herd }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
356
+ }
357
+ function herdList(a = {}, o = {}) {
358
+ const payload = {};
359
+ if (a.all !== undefined)
360
+ payload.all = a.all;
361
+ return rtCommand("herd:list", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
362
+ }
363
+ function herdResume(a, o = {}) {
364
+ return rtCommand("herd:resume", { herd: a.herd, session: a.session }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
365
+ }
366
+ function herdClose(a, o = {}) {
367
+ return rtCommand("herd:close", { herd: a.herd, job: a.job }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30000 });
368
+ }
369
+ function herdAttend(a, o = {}) {
370
+ return rtCommand("herd:attend", { herd: a.herd, job: a.job, callerWorkspace: a.callerWorkspace }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 60000 });
371
+ }
372
+ function herdWrapUp(a, o = {}) {
373
+ const payload = { herd: a.herd };
374
+ for (const k of ["closePanes", "dispose", "deleteJobDirs", "archiveRoom"])
375
+ if (a[k] !== undefined)
376
+ payload[k] = a[k];
377
+ return rtCommand("herd:wrap-up", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 120000 });
378
+ }
379
+ function herdStopHidden(_a, o = {}) {
380
+ return rtCommand("herd:stop-hidden", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30000 });
381
+ }
382
+ function bgEnsure(a = {}, o = {}) {
383
+ const payload = {};
384
+ if (a.claim !== undefined)
385
+ payload.claim = a.claim;
386
+ return rtCommand("bg:ensure", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 15000 });
387
+ }
388
+ function bgStatus(o = {}) {
389
+ return rtCommand("bg:status", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
390
+ }
391
+ function bgStop(o = {}) {
392
+ return rtCommand("bg:stop", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30000 });
393
+ }
394
+ function bgRelease(a, o = {}) {
395
+ return rtCommand("bg:release", { claim: a.claim }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
396
+ }
315
397
  // src/commands.ts
316
398
  var GATE_BY_PANE = "pane";
317
399
  function gateOptionValue(o) {
@@ -397,6 +479,20 @@ var COMMAND_NAMES = [
397
479
  "gate:subscribe",
398
480
  "gate:unsubscribe",
399
481
  "gate:subscriptions",
482
+ "herd:start",
483
+ "herd:resume",
484
+ "herd:status",
485
+ "herd:list",
486
+ "herd:close",
487
+ "herd:spawn",
488
+ "herd:gates",
489
+ "herd:ask",
490
+ "herd:milestone",
491
+ "herd:answer",
492
+ "herd:report",
493
+ "herd:attend",
494
+ "herd:stop-hidden",
495
+ "herd:wrap-up",
400
496
  "hooks:repair",
401
497
  "hooks:watch",
402
498
  "sdm:catalog",
@@ -410,7 +506,11 @@ var COMMAND_NAMES = [
410
506
  "worktree:list",
411
507
  "worktree:restore",
412
508
  "worktree:freshen",
413
- "worktree:adopt"
509
+ "worktree:adopt",
510
+ "bg:ensure",
511
+ "bg:status",
512
+ "bg:stop",
513
+ "bg:release"
414
514
  ];
415
515
  // src/relay.ts
416
516
  var DEFAULT_WS_URL = "ws://127.0.0.1:9401/ws";
@@ -587,6 +687,26 @@ async function openSmartPane(herdr, anchorPaneId, opts = {}) {
587
687
  }
588
688
  return { paneId, placement };
589
689
  }
690
+ // src/pane-ref.ts
691
+ var BG_PREFIX = "bg:";
692
+ function parsePaneRef(ref) {
693
+ if (ref.startsWith(BG_PREFIX)) {
694
+ return {
695
+ server: "bg",
696
+ paneId: ref.slice(BG_PREFIX.length)
697
+ };
698
+ }
699
+ return {
700
+ server: "visible",
701
+ paneId: ref
702
+ };
703
+ }
704
+ function formatPaneRef(paneId, server) {
705
+ if (server === "visible") {
706
+ return paneId;
707
+ }
708
+ return BG_PREFIX + paneId;
709
+ }
590
710
  // src/settings/resolve.ts
591
711
  import { homedir as homedir4 } from "os";
592
712
  import { join as join5 } from "path";
@@ -604,6 +724,9 @@ function userSettingsPath() {
604
724
  function teamSettingsPath(team) {
605
725
  return join3(teamsDir(), team, "mattstack", "settings.team.jsonc");
606
726
  }
727
+ function teamLocalPath(team) {
728
+ return join3(home(), ".mattstack", "rt", "teams", `${team}.json`);
729
+ }
607
730
  function machineSettingsPath() {
608
731
  return join3(home(), ".mattstack", "user", "local", machineKey(), "settings.local.jsonc");
609
732
  }
@@ -696,7 +819,7 @@ var REGISTRY = [
696
819
  scopes: ["user"],
697
820
  default: [],
698
821
  merge: "replace",
699
- 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."
822
+ description: "Event-bus glob rules that raise a desktop notification: [{pattern, category, title, message, subjectPrefix?, url?}]. pattern is matched against the events-bus topic (Bun.Glob semantics); title/message may interpolate `{field}` from the event payload, plus the computed `{question}` field (the event payload's first question label, `payload.questions[0].label`, empty string when absent); optional subjectPrefix matches the event payload's subject as a prefix. The optional url is interpolated the same way as title/message and becomes the notification's Open target; a gate rule should set it. A fresh key, not an ownership-latch port, so a default is fine here."
700
823
  },
701
824
  {
702
825
  key: "rt.cron",
@@ -1712,10 +1835,23 @@ function explainSetting(key, opts = {}) {
1712
1835
  return resolveDef(def, readStores(), opts).rows;
1713
1836
  }
1714
1837
  // src/settings/write.ts
1715
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync4, renameSync, unlinkSync, writeFileSync } from "fs";
1838
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync5, renameSync, unlinkSync, writeFileSync } from "fs";
1716
1839
  import { applyEdits, modify, parseTree } from "jsonc-parser";
1717
1840
  import { randomBytes } from "crypto";
1718
1841
  import { dirname as dirname2 } from "path";
1842
+
1843
+ // src/settings/team-local-read.ts
1844
+ import { readFileSync as readFileSync4 } from "fs";
1845
+ function isJoinedTeam(team) {
1846
+ try {
1847
+ const parsed = JSON.parse(readFileSync4(teamLocalPath(team), "utf8"));
1848
+ return typeof parsed === "object" && parsed !== null && parsed.joinedByRt === true;
1849
+ } catch {
1850
+ return false;
1851
+ }
1852
+ }
1853
+
1854
+ // src/settings/write.ts
1719
1855
  var FORMAT = { tabSize: 2, insertSpaces: true, eol: `
1720
1856
  ` };
1721
1857
  function refuse(message) {
@@ -1773,6 +1909,11 @@ function migratedFalseMessage(key, def) {
1773
1909
  const legacyPart = def.legacyFile ? ` — it is still read from ${def.legacyFile}` : "";
1774
1910
  return `"${key}" is not writable through the settings resolver yet${legacyPart}`;
1775
1911
  }
1912
+ function refuseIfJoined(team) {
1913
+ if (isJoinedTeam(team)) {
1914
+ refuse(`this machine joined "${team}" by invite, so its clone is pull-only and team settings cannot be written here. Ask the team's owner to make this change. Member-proposed changes are tracked in MAT-415.`);
1915
+ }
1916
+ }
1776
1917
  function resolveStorePath(scope, opts) {
1777
1918
  if (scope === "user")
1778
1919
  return userSettingsPath();
@@ -1783,6 +1924,7 @@ function resolveStorePath(scope, opts) {
1783
1924
  if (!existsSync3(path)) {
1784
1925
  refuse(`team store for "${opts.team}" does not exist (${path}) — clone/seed it before writing to it`);
1785
1926
  }
1927
+ refuseIfJoined(opts.team);
1786
1928
  return path;
1787
1929
  }
1788
1930
  const teams = listTeams();
@@ -1792,7 +1934,9 @@ function resolveStorePath(scope, opts) {
1792
1934
  if (teams.length > 1) {
1793
1935
  refuse(`multiple local team stores found (${teams.join(", ")}) — pass opts.team to choose one`);
1794
1936
  }
1795
- return teamSettingsPath(teams[0]);
1937
+ const team = teams[0];
1938
+ refuseIfJoined(team);
1939
+ return teamSettingsPath(team);
1796
1940
  }
1797
1941
  function resolveStorePathForUnset(scope, opts) {
1798
1942
  if (scope === "user")
@@ -1801,7 +1945,10 @@ function resolveStorePathForUnset(scope, opts) {
1801
1945
  return machineSettingsPath();
1802
1946
  if (opts.team !== undefined) {
1803
1947
  const path = teamSettingsPath(opts.team);
1804
- return existsSync3(path) ? path : null;
1948
+ if (!existsSync3(path))
1949
+ return null;
1950
+ refuseIfJoined(opts.team);
1951
+ return path;
1805
1952
  }
1806
1953
  const teams = listTeams();
1807
1954
  if (teams.length === 0)
@@ -1809,7 +1956,9 @@ function resolveStorePathForUnset(scope, opts) {
1809
1956
  if (teams.length > 1) {
1810
1957
  refuse(`multiple local team stores found (${teams.join(", ")}) — pass opts.team to choose one`);
1811
1958
  }
1812
- return teamSettingsPath(teams[0]);
1959
+ const team = teams[0];
1960
+ refuseIfJoined(team);
1961
+ return teamSettingsPath(team);
1813
1962
  }
1814
1963
  function seedHeader() {
1815
1964
  return `// rt settings — created by \`rt settings set\`. JSONC: comments and trailing commas are fine.
@@ -1855,7 +2004,7 @@ function findDuplicateKey(node) {
1855
2004
  function writeIntoStore(storePath, jsonPath, value, createIfMissing) {
1856
2005
  let content;
1857
2006
  if (existsSync3(storePath)) {
1858
- content = readFileSync4(storePath, "utf8");
2007
+ content = readFileSync5(storePath, "utf8");
1859
2008
  if (content.trim() === "") {
1860
2009
  content = seedHeader();
1861
2010
  } else {
@@ -1876,7 +2025,7 @@ function writeIntoStore(storePath, jsonPath, value, createIfMissing) {
1876
2025
  writeTempThenRename(storePath, finalText);
1877
2026
  }
1878
2027
  function removeFromStore(storePath, jsonPath) {
1879
- const content = readFileSync4(storePath, "utf8");
2028
+ const content = readFileSync5(storePath, "utf8");
1880
2029
  if (content.trim() === "")
1881
2030
  return false;
1882
2031
  assertEditableJsonc(storePath, content);
@@ -1903,7 +2052,7 @@ function writeTempThenRename(storePath, finalText) {
1903
2052
  }
1904
2053
  }
1905
2054
  // src/settings/identity.ts
1906
- import { existsSync as existsSync4, readFileSync as readFileSync5, realpathSync } from "fs";
2055
+ import { existsSync as existsSync4, readFileSync as readFileSync6, realpathSync } from "fs";
1907
2056
 
1908
2057
  // src/settings/exec.ts
1909
2058
  async function runCapture(argv, opts = {}) {
@@ -2065,7 +2214,7 @@ async function resolveNameToIdentity(name, reposJsonPath) {
2065
2214
  if (!existsSync4(reposJsonPath))
2066
2215
  return null;
2067
2216
  try {
2068
- const index = JSON.parse(readFileSync5(reposJsonPath, "utf8"));
2217
+ const index = JSON.parse(readFileSync6(reposJsonPath, "utf8"));
2069
2218
  const path = index[name];
2070
2219
  if (typeof path !== "string")
2071
2220
  return null;
@@ -2090,6 +2239,7 @@ export {
2090
2239
  readMrsByBranch,
2091
2240
  readDiscussions,
2092
2241
  readBranchCache,
2242
+ parsePaneRef,
2093
2243
  parseIdentity,
2094
2244
  paneSpawn,
2095
2245
  paneSend,
@@ -2105,6 +2255,20 @@ export {
2105
2255
  listRuns,
2106
2256
  isMigrated,
2107
2257
  identityFromRemote,
2258
+ herdWrapUp,
2259
+ herdStopHidden,
2260
+ herdStatus,
2261
+ herdStart,
2262
+ herdSpawn,
2263
+ herdResume,
2264
+ herdReport,
2265
+ herdMilestone,
2266
+ herdList,
2267
+ herdGates,
2268
+ herdClose,
2269
+ herdAttend,
2270
+ herdAsk,
2271
+ herdAnswer,
2108
2272
  getSetting,
2109
2273
  getRun,
2110
2274
  getDef,
@@ -2119,6 +2283,7 @@ export {
2119
2283
  gateList,
2120
2284
  gateClose,
2121
2285
  gateAnswer,
2286
+ formatPaneRef,
2122
2287
  explainSetting,
2123
2288
  expandVariables,
2124
2289
  eventsWait,
@@ -2150,6 +2315,10 @@ export {
2150
2315
  chatAway,
2151
2316
  chatArchive,
2152
2317
  chatAck,
2318
+ bgStop,
2319
+ bgStatus,
2320
+ bgRelease,
2321
+ bgEnsure,
2153
2322
  allDefs,
2154
2323
  agentStart,
2155
2324
  agentResume,
@@ -2161,5 +2330,6 @@ export {
2161
2330
  GATE_BY_PANE,
2162
2331
  DEFAULT_WS_URL,
2163
2332
  DEFAULT_SOCK,
2164
- COMMAND_NAMES
2333
+ COMMAND_NAMES,
2334
+ BG_PREFIX
2165
2335
  };
@@ -0,0 +1,8 @@
1
+ export declare const BG_PREFIX = "bg:";
2
+ export type PaneServer = "visible" | "bg";
3
+ export interface PaneRef {
4
+ server: PaneServer;
5
+ paneId: string;
6
+ }
7
+ export declare function parsePaneRef(ref: string): PaneRef;
8
+ export declare function formatPaneRef(paneId: string, server: PaneServer): string;
@@ -12,6 +12,14 @@
12
12
  export declare function userSettingsPath(): string;
13
13
  /** ~/.mattstack/teams/<team>/mattstack/settings.team.jsonc — the team store. */
14
14
  export declare function teamSettingsPath(team: string): string;
15
+ /**
16
+ * ~/.mattstack/rt/teams/<team>.json: the machine-local team record. Mirrored
17
+ * from repo-tools/lib/team/team-local.ts's teamLocalPath, which is the
18
+ * authority: rt-client has no dependency on rt's lib/, so this literal is
19
+ * duplicated here rather than imported (same convention as `teamSettingsPath`
20
+ * and `userSettingsPath` above).
21
+ */
22
+ export declare function teamLocalPath(team: string): string;
15
23
  /**
16
24
  * ~/.mattstack/user/local/<machineKey()>/settings.local.jsonc — the machine
17
25
  * store, TRACKED and keyed per machine (path literals legal here only).
@@ -0,0 +1,7 @@
1
+ /**
2
+ * One field of the machine-local team record, for the write guard. The record
3
+ * itself is owned by repo-tools/lib/team/team-local.ts; this reads only what
4
+ * the guard needs and never writes.
5
+ */
6
+ /** Unreadable, absent or malformed all read as false, so nothing that predates the field is refused. */
7
+ export declare function isJoinedTeam(team: string): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mattstack/rt-client",
3
- "version": "0.16.0",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/client.ts CHANGED
@@ -356,7 +356,7 @@ export function agentStart(
356
356
  a: Commands["agent:start"]["payload"], o: RtClientOptions = {},
357
357
  ): Promise<RtResponse<AgentRecord>> {
358
358
  const payload: Record<string, unknown> = { repo: a.repo, cwd: a.cwd };
359
- for (const k of ["prompt", "surface", "model", "effort", "account", "label", "caller", "workspace", "tab", "extraArgs"] as const) {
359
+ for (const k of ["prompt", "surface", "model", "effort", "account", "label", "caller", "workspace", "tab", "extraArgs", "env", "herdrSocket", "handle", "bg"] as const) {
360
360
  if (a[k] !== undefined) payload[k] = a[k];
361
361
  }
362
362
  return rtCommand<AgentRecord>("agent:start", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30_000 });
@@ -441,7 +441,9 @@ export function paneFocus(
441
441
  a: Commands["pane:focus"]["payload"],
442
442
  o: RtClientOptions = {},
443
443
  ): Promise<RtResponse<Commands["pane:focus"]["data"]>> {
444
- return rtCommand<Commands["pane:focus"]["data"]>("pane:focus", { paneId: a.paneId }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
444
+ const payload: Record<string, unknown> = { paneId: a.paneId };
445
+ if (a.callerWorkspace !== undefined) payload.callerWorkspace = a.callerWorkspace;
446
+ return rtCommand<Commands["pane:focus"]["data"]>("pane:focus", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
445
447
  }
446
448
 
447
449
  // ─── Gates (BOARD-20/21 gate facility) ─────────────────────────────────────
@@ -519,3 +521,153 @@ export function gateSubscriptions(
519
521
  for (const k of ["session", "live"] as const) if (a[k] !== undefined) payload[k] = a[k];
520
522
  return rtCommand<Commands["gate:subscriptions"]["data"]>("gate:subscriptions", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
521
523
  }
524
+
525
+ // ─── Herd (shepherd run registry) ───────────────────────────────────────────
526
+
527
+ /** Provisions the room, workspace and subscription before it answers, so it
528
+ gets the same 60s budget as a spawn rather than the 10s default. */
529
+ export function herdStart(
530
+ a: Commands["herd:start"]["payload"],
531
+ o: RtClientOptions = {},
532
+ ): Promise<RtResponse<Commands["herd:start"]["data"]>> {
533
+ const payload: Record<string, unknown> = { name: a.name, repo: a.repo, session: a.session };
534
+ if (a.hidden !== undefined) payload.hidden = a.hidden;
535
+ return rtCommand<Commands["herd:start"]["data"]>("herd:start", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 60_000 });
536
+ }
537
+
538
+ /** Worktree provision plus agent launch; the 10s default cannot cover it. */
539
+ export function herdSpawn(
540
+ a: Commands["herd:spawn"]["payload"],
541
+ o: RtClientOptions = {},
542
+ ): Promise<RtResponse<Commands["herd:spawn"]["data"]>> {
543
+ const payload: Record<string, unknown> = { herd: a.herd, job: a.job };
544
+ for (const k of ["brief", "dir", "model", "effort", "account", "disposable"] as const) if (a[k] !== undefined) payload[k] = a[k];
545
+ return rtCommand<Commands["herd:spawn"]["data"]>("herd:spawn", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 60_000 });
546
+ }
547
+
548
+ export function herdAsk(
549
+ a: Commands["herd:ask"]["payload"],
550
+ o: RtClientOptions = {},
551
+ ): Promise<RtResponse<Commands["herd:ask"]["data"]>> {
552
+ const payload: Record<string, unknown> = { herd: a.herd, job: a.job, session: a.session, questions: a.questions };
553
+ for (const k of ["pane", "context"] as const) if (a[k] !== undefined) payload[k] = a[k];
554
+ return rtCommand<Commands["herd:ask"]["data"]>("herd:ask", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
555
+ }
556
+
557
+ export function herdMilestone(
558
+ a: Commands["herd:milestone"]["payload"],
559
+ o: RtClientOptions = {},
560
+ ): Promise<RtResponse<Commands["herd:milestone"]["data"]>> {
561
+ const payload: Record<string, unknown> = { herd: a.herd, job: a.job, session: a.session, artifact: a.artifact };
562
+ for (const k of ["pane", "summary"] as const) if (a[k] !== undefined) payload[k] = a[k];
563
+ return rtCommand<Commands["herd:milestone"]["data"]>("herd:milestone", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
564
+ }
565
+
566
+ export function herdAnswer(
567
+ a: Commands["herd:answer"]["payload"],
568
+ o: RtClientOptions = {},
569
+ ): Promise<RtResponse<Commands["herd:answer"]["data"]>> {
570
+ return rtCommand<Commands["herd:answer"]["data"]>("herd:answer", { gate: a.gate }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
571
+ }
572
+
573
+ /** A disposable job's report also closes its pane, one herdr CLI call under the runner's own 15s budget. */
574
+ export function herdReport(
575
+ a: Commands["herd:report"]["payload"],
576
+ o: RtClientOptions = {},
577
+ ): Promise<RtResponse<Commands["herd:report"]["data"]>> {
578
+ return rtCommand<Commands["herd:report"]["data"]>("herd:report", { herd: a.herd, job: a.job, body: a.body }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30_000 });
579
+ }
580
+
581
+ export function herdGates(
582
+ a: Commands["herd:gates"]["payload"],
583
+ o: RtClientOptions = {},
584
+ ): Promise<RtResponse<Commands["herd:gates"]["data"]>> {
585
+ return rtCommand<Commands["herd:gates"]["data"]>("herd:gates", { herd: a.herd }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
586
+ }
587
+
588
+ export function herdStatus(
589
+ a: Commands["herd:status"]["payload"],
590
+ o: RtClientOptions = {},
591
+ ): Promise<RtResponse<Commands["herd:status"]["data"]>> {
592
+ return rtCommand<Commands["herd:status"]["data"]>("herd:status", { herd: a.herd }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
593
+ }
594
+
595
+ export function herdList(
596
+ a: Commands["herd:list"]["payload"] = {},
597
+ o: RtClientOptions = {},
598
+ ): Promise<RtResponse<Commands["herd:list"]["data"]>> {
599
+ const payload: Record<string, unknown> = {};
600
+ if (a.all !== undefined) payload.all = a.all;
601
+ return rtCommand<Commands["herd:list"]["data"]>("herd:list", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
602
+ }
603
+
604
+ export function herdResume(
605
+ a: Commands["herd:resume"]["payload"],
606
+ o: RtClientOptions = {},
607
+ ): Promise<RtResponse<Commands["herd:resume"]["data"]>> {
608
+ return rtCommand<Commands["herd:resume"]["data"]>("herd:resume", { herd: a.herd, session: a.session }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
609
+ }
610
+
611
+ /** Closes a herdr pane, one CLI call under the runner's own 15s budget. */
612
+ export function herdClose(
613
+ a: Commands["herd:close"]["payload"],
614
+ o: RtClientOptions = {},
615
+ ): Promise<RtResponse<Commands["herd:close"]["data"]>> {
616
+ return rtCommand<Commands["herd:close"]["data"]>("herd:close", { herd: a.herd, job: a.job }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30_000 });
617
+ }
618
+
619
+ /** Three sequential herdr CLI calls (pane get, tab create, pane run), each under the runner's own 15s budget. */
620
+ export function herdAttend(
621
+ a: Commands["herd:attend"]["payload"],
622
+ o: RtClientOptions = {},
623
+ ): Promise<RtResponse<Commands["herd:attend"]["data"]>> {
624
+ return rtCommand<Commands["herd:attend"]["data"]>("herd:attend", { herd: a.herd, job: a.job, callerWorkspace: a.callerWorkspace }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 60_000 });
625
+ }
626
+
627
+ /** Closes panes, disposes worktrees and archives the room in one pass; the
628
+ worktree disposals alone can outrun a spawn's budget. */
629
+ export function herdWrapUp(
630
+ a: Commands["herd:wrap-up"]["payload"],
631
+ o: RtClientOptions = {},
632
+ ): Promise<RtResponse<Commands["herd:wrap-up"]["data"]>> {
633
+ const payload: Record<string, unknown> = { herd: a.herd };
634
+ for (const k of ["closePanes", "dispose", "deleteJobDirs", "archiveRoom"] as const) if (a[k] !== undefined) payload[k] = a[k];
635
+ return rtCommand<Commands["herd:wrap-up"]["data"]>("herd:wrap-up", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 120_000 });
636
+ }
637
+
638
+ /** Runs `herdr session stop`, one CLI call under the runner's own 15s budget. */
639
+ export function herdStopHidden(
640
+ _a: Commands["herd:stop-hidden"]["payload"],
641
+ o: RtClientOptions = {},
642
+ ): Promise<RtResponse<Commands["herd:stop-hidden"]["data"]>> {
643
+ return rtCommand<Commands["herd:stop-hidden"]["data"]>("herd:stop-hidden", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30_000 });
644
+ }
645
+
646
+ // ─── Background server (daemon-owned background herdr session) ────────────
647
+
648
+ /** May spawn `herdr server` and wait for it to bind; budget matches bg-service's own 10s readyTimeoutMs plus margin. */
649
+ export function bgEnsure(
650
+ a: Commands["bg:ensure"]["payload"] = {},
651
+ o: RtClientOptions = {},
652
+ ): Promise<RtResponse<Commands["bg:ensure"]["data"]>> {
653
+ const payload: Record<string, unknown> = {};
654
+ if (a.claim !== undefined) payload.claim = a.claim;
655
+ return rtCommand<Commands["bg:ensure"]["data"]>("bg:ensure", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 15_000 });
656
+ }
657
+
658
+ /** Never ensures/spawns; a plain read of the current state. */
659
+ export function bgStatus(o: RtClientOptions = {}): Promise<RtResponse<Commands["bg:status"]["data"]>> {
660
+ return rtCommand<Commands["bg:status"]["data"]>("bg:status", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
661
+ }
662
+
663
+ /** Asks the daemon to stop the background server; refuses (ok:false) while any claim is live, naming the owners. */
664
+ export function bgStop(o: RtClientOptions = {}): Promise<RtResponse<Commands["bg:stop"]["data"]>> {
665
+ return rtCommand<Commands["bg:stop"]["data"]>("bg:stop", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30_000 });
666
+ }
667
+
668
+ export function bgRelease(
669
+ a: Commands["bg:release"]["payload"],
670
+ o: RtClientOptions = {},
671
+ ): Promise<RtResponse<Commands["bg:release"]["data"]>> {
672
+ return rtCommand<Commands["bg:release"]["data"]>("bg:release", { claim: a.claim }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
673
+ }
package/src/commands.ts CHANGED
@@ -134,6 +134,21 @@ export interface GateSubscription {
134
134
  dead: boolean;
135
135
  }
136
136
 
137
+ export interface HerdInfo { id: string; repo: string; room: string; workspace: string; shepherdSession: string; shepherdHandle: string; herdrSocket: string | null; hidden: boolean; status: "active" | "wrapped"; createdAt: number; wrappedAt: number | null }
138
+ /** A herd row as `herd:list` reports it: the registry row plus how many jobs hang off it. */
139
+ export interface HerdListRow extends HerdInfo { jobs: number }
140
+ export interface HerdJobInfo { herd: string; name: string; worktree: string; branch: string | null; tree: string | null; pane: string | null; agentSession: string | null; agentId: string | null; handle: string; status: "spawning" | "active" | "at-gate" | "at-milestone" | "done" | "closed" | "crashed"; disposable: boolean; lastGate: string | null; lastReport: number | null; createdAt: number; updatedAt: number }
141
+ /** `lastGateStatus`/`lastGateDelivery` come from the job's `lastGate` row: an `answered` gate whose delivery is `dead-pane` is the "answered, worker not woken" case the shepherd must act on. */
142
+ export interface HerdStatusData {
143
+ herd: HerdInfo;
144
+ jobs: Array<HerdJobInfo & { openGate: string | null; paneStatus: string | null; lastGateStatus: GateStatus | null; lastGateDelivery: "delivered" | "dead-pane" | null }>;
145
+ unread: number;
146
+ lifecycleConnected: boolean;
147
+ hiddenUp: boolean | null;
148
+ /** The shepherd session's own `herd:<id>/` subscription row, or null when none is live. */
149
+ subscription: { id: string; dead: boolean; lastDelivery: GateSubscription["lastDelivery"] } | null;
150
+ }
151
+
137
152
  /**
138
153
  * Duplicated shape on purpose, same reasoning as EventsBusEvent above:
139
154
  * these mirror lib/state/chat-store.ts's types, which rt-client cannot
@@ -229,7 +244,10 @@ export interface InviteResult { paneId: string; delivered: "accepted" | "queued"
229
244
  /** Duplicated shape on purpose: mirrors lib/daemon/inject.ts's InjectResult. */
230
245
  export type PaneDelivery = "accepted" | "queued" | "refused";
231
246
  export interface PaneSendResult { paneId: string; delivered: PaneDelivery; reason?: string }
232
- export interface PaneFocusResult { paneId: string; focused: boolean }
247
+ /** `attendTab` is set only for a `bg:` ref: focus for a background pane IS
248
+ the attend flow (a visible tab running a terminal attach), and this is
249
+ that tab's id. */
250
+ export interface PaneFocusResult { paneId: string; focused: boolean; attendTab?: string }
233
251
 
234
252
  // SKILLS-53: one judgment, computed once in rt, so the console and the tray
235
253
  // never derive two verdicts that can disagree.
@@ -540,7 +558,7 @@ export interface Commands {
540
558
  "chat:dm-open": { payload: { from: string; to: string; sessionId?: string }; data: { room: string; created: boolean } };
541
559
 
542
560
  // ─── Agent handoff (rt agent) ────────────────────────────────────────────
543
- "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 };
561
+ "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; env?: Record<string, string>; herdrSocket?: string; handle?: string; bg?: boolean }; data: AgentRecord };
544
562
  "agent:resume": { payload: { id: string; prompt?: string; surface?: AgentSurface; workspace?: string; tab?: string }; data: AgentRecord };
545
563
  "agent:get": { payload: { id: string }; data: AgentRecord };
546
564
  "agent:list": { payload: { repo?: string }; data: { agents: AgentRecord[] } };
@@ -554,7 +572,9 @@ export interface Commands {
554
572
  data: { pane: ChatPane; ready: boolean };
555
573
  };
556
574
  "pane:send": { payload: { paneId: string; text: string; callerPane?: string }; data: PaneSendResult };
557
- "pane:focus": { payload: { paneId: string }; data: PaneFocusResult };
575
+ /** `callerWorkspace` (HERDR_WORKSPACE_ID) is required only for a `bg:`
576
+ ref, whose focus opens an attend tab in the caller's own workspace. */
577
+ "pane:focus": { payload: { paneId: string; callerWorkspace?: string }; data: PaneFocusResult };
558
578
 
559
579
  // ─── R013/R016 ────────────────────────────────────────────────
560
580
  "cache:read": { payload: { branches?: string[]; maxAgeMs?: number; repoIdentity?: string }; data: Record<string, BranchEnrichment> };
@@ -612,6 +632,25 @@ export interface Commands {
612
632
  * onto delivery outcomes (dead marks included). */
613
633
  "gate:subscriptions": { payload: { session?: string; live?: boolean }; data: { subscriptions: GateSubscription[] } };
614
634
 
635
+ // ─── Herd (shepherd run registry) ────────────────────────────────────────
636
+ "herd:start": { payload: { name: string; repo: string; session: string; hidden?: boolean }; data: { herd: string; room: string; workspace: string; subscription: string; handle: string; hidden: boolean } };
637
+ "herd:resume": { payload: { herd: string; session: string }; data: { subscription: string; gates: GateRow[]; unread: number; status: HerdStatusData; handle: string } };
638
+ "herd:status": { payload: { herd: string }; data: HerdStatusData };
639
+ /** Active herds only unless `all`, so a shepherd's "which herd am I on" question has one answer. */
640
+ "herd:list": { payload: { all?: boolean }; data: { herds: HerdListRow[] } };
641
+ "herd:close": { payload: { herd: string; job: string }; data: { job: string; status: "closed" } };
642
+ /** `brief` is the brief TEXT, not a path: the CLI reads the file. It is stored at `<jobsRoot>/<herd>/<job>/job.md`, so a respawn with `dir` and no `brief` reads it back. */
643
+ "herd:spawn": { payload: { herd: string; job: string; brief?: string; dir?: string; model?: string; effort?: string; account?: string; disposable?: boolean }; data: { herd: string; job: string; pane: string; worktree: string; branch: string | null; tree: string | null; /** null = no provisioning ran (--dir); false = cold create, worth announcing. */ wasOnDeck: boolean | null; agentId: string; sessionId: string; handle: string } };
644
+ "herd:gates": { payload: { herd: string }; data: { gates: GateRow[] } };
645
+ "herd:ask": { payload: { herd: string; job: string; session: string; pane?: string; questions: GateQuestion[]; context?: string }; data: { gate: string } };
646
+ "herd:milestone": { payload: { herd: string; job: string; session: string; pane?: string; artifact: string; summary?: string }; data: { gate: string; message: number } };
647
+ "herd:answer": { payload: { gate: string }; data: { gate: string; status: GateStatus; answer: GateAnswer | null; closedReason: GateRow["closedReason"] } };
648
+ "herd:report": { payload: { herd: string; job: string; body: string }; data: { message: number } };
649
+ /** `callerWorkspace` is the attending session's own HERDR_WORKSPACE_ID: the attached tab opens there, not in the herd's workspace. */
650
+ "herd:attend": { payload: { herd: string; job: string; callerWorkspace: string }; data: { tab: string; pane: string } };
651
+ "herd:stop-hidden": { payload: Record<string, never>; data: { stopped: boolean } };
652
+ "herd:wrap-up": { payload: { herd: string; closePanes?: boolean; dispose?: string[]; deleteJobDirs?: boolean; archiveRoom?: boolean }; data: { closed: string[]; workspaceClosed: boolean; disposed: string[]; refused: Array<{ tree: string; reason: string }>; deletedJobDirs: boolean; archived: boolean } };
653
+
615
654
  /** Wire reply on success is always `{ok:true, repaired}` (no `data`
616
655
  * wrapper) — `data` here documents the extra field the same way PingData
617
656
  * does for `ping`, not the literal wire nesting (R3). */
@@ -632,6 +671,13 @@ export interface Commands {
632
671
  "worktree:restore": { payload: { repoName: string; tree: string }; data: WorktreeRestoreData };
633
672
  "worktree:freshen": { payload: { repoName?: string; tree?: string }; data: WorktreeFreshenData };
634
673
  "worktree:adopt": { payload: { repoName: string; claim?: boolean }; data: WorktreeAdoptData };
674
+
675
+ // ─── Background server (daemon-owned background herdr session) ──────────
676
+ "bg:ensure": { payload: { claim?: string }; data: { socket: string; started: boolean; parity: { ok: boolean; drift: string[] } | null } };
677
+ "bg:status": { payload: Record<string, never>; data: { up: boolean; socket: string; claims: Array<{ owner: string; pane: string | null; createdAt: number }> } };
678
+ /** Rejects (`ok:false`) naming every live claim owner while any claim is held. */
679
+ "bg:stop": { payload: Record<string, never>; data: { stopped: boolean } };
680
+ "bg:release": { payload: { claim: string }; data: { released: boolean } };
635
681
  }
636
682
 
637
683
  export type CommandName = keyof Commands;
@@ -715,6 +761,20 @@ export const COMMAND_NAMES: readonly CommandName[] = [
715
761
  "gate:subscribe",
716
762
  "gate:unsubscribe",
717
763
  "gate:subscriptions",
764
+ "herd:start",
765
+ "herd:resume",
766
+ "herd:status",
767
+ "herd:list",
768
+ "herd:close",
769
+ "herd:spawn",
770
+ "herd:gates",
771
+ "herd:ask",
772
+ "herd:milestone",
773
+ "herd:answer",
774
+ "herd:report",
775
+ "herd:attend",
776
+ "herd:stop-hidden",
777
+ "herd:wrap-up",
718
778
  "hooks:repair",
719
779
  "hooks:watch",
720
780
  "sdm:catalog",
@@ -729,4 +789,9 @@ export const COMMAND_NAMES: readonly CommandName[] = [
729
789
  "worktree:restore",
730
790
  "worktree:freshen",
731
791
  "worktree:adopt",
792
+
793
+ "bg:ensure",
794
+ "bg:status",
795
+ "bg:stop",
796
+ "bg:release",
732
797
  ];
package/src/index.ts CHANGED
@@ -54,6 +54,24 @@ export {
54
54
  gateSubscribe,
55
55
  gateUnsubscribe,
56
56
  gateSubscriptions,
57
+ herdStart,
58
+ herdSpawn,
59
+ herdAsk,
60
+ herdMilestone,
61
+ herdAnswer,
62
+ herdReport,
63
+ herdGates,
64
+ herdStatus,
65
+ herdList,
66
+ herdResume,
67
+ herdClose,
68
+ herdAttend,
69
+ herdWrapUp,
70
+ herdStopHidden,
71
+ bgEnsure,
72
+ bgStatus,
73
+ bgStop,
74
+ bgRelease,
57
75
  } from "./client.ts";
58
76
 
59
77
  export { COMMAND_NAMES, GATE_BY_PANE, gateOptionValue, gateOptionLabel } from "./commands.ts";
@@ -100,6 +118,10 @@ export type {
100
118
  GateAnswer,
101
119
  GateRow,
102
120
  GateSubscription,
121
+ HerdInfo,
122
+ HerdListRow,
123
+ HerdJobInfo,
124
+ HerdStatusData,
103
125
  } from "./commands.ts";
104
126
 
105
127
  export { subscribe, createRelay, DEFAULT_WS_URL } from "./relay.ts";
@@ -112,6 +134,9 @@ export { repoNameForPath } from "./repos.ts";
112
134
  export { decidePlacement, openSmartPane } from "./smart-pane.ts";
113
135
  export type { Placement, PlacementOpts, HerdrCall } from "./smart-pane.ts";
114
136
 
137
+ export { BG_PREFIX, parsePaneRef, formatPaneRef } from "./pane-ref.ts";
138
+ export type { PaneServer, PaneRef } from "./pane-ref.ts";
139
+
115
140
  // ─── Settings (RT-50) ────────────────────────────────────────────────────────
116
141
 
117
142
  export { getSetting, listSettings, explainSetting, expandVariables, SCOPE_ORDER, setSettingsWarnSink } from "./settings/resolve.ts";
@@ -0,0 +1,28 @@
1
+ export const BG_PREFIX = "bg:";
2
+
3
+ export type PaneServer = "visible" | "bg";
4
+
5
+ export interface PaneRef {
6
+ server: PaneServer;
7
+ paneId: string;
8
+ }
9
+
10
+ export function parsePaneRef(ref: string): PaneRef {
11
+ if (ref.startsWith(BG_PREFIX)) {
12
+ return {
13
+ server: "bg",
14
+ paneId: ref.slice(BG_PREFIX.length),
15
+ };
16
+ }
17
+ return {
18
+ server: "visible",
19
+ paneId: ref,
20
+ };
21
+ }
22
+
23
+ export function formatPaneRef(paneId: string, server: PaneServer): string {
24
+ if (server === "visible") {
25
+ return paneId;
26
+ }
27
+ return BG_PREFIX + paneId;
28
+ }
@@ -27,6 +27,17 @@ export function teamSettingsPath(team: string): string {
27
27
  return join(teamsDir(), team, "mattstack", "settings.team.jsonc");
28
28
  }
29
29
 
30
+ /**
31
+ * ~/.mattstack/rt/teams/<team>.json: the machine-local team record. Mirrored
32
+ * from repo-tools/lib/team/team-local.ts's teamLocalPath, which is the
33
+ * authority: rt-client has no dependency on rt's lib/, so this literal is
34
+ * duplicated here rather than imported (same convention as `teamSettingsPath`
35
+ * and `userSettingsPath` above).
36
+ */
37
+ export function teamLocalPath(team: string): string {
38
+ return join(home(), ".mattstack", "rt", "teams", `${team}.json`);
39
+ }
40
+
30
41
  /**
31
42
  * ~/.mattstack/user/local/<machineKey()>/settings.local.jsonc — the machine
32
43
  * store, TRACKED and keyed per machine (path literals legal here only).
@@ -85,7 +85,7 @@ export const REGISTRY: readonly SettingDef[] = [
85
85
  scopes: ["user"],
86
86
  default: [],
87
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.",
88
+ description: "Event-bus glob rules that raise a desktop notification: [{pattern, category, title, message, subjectPrefix?, url?}]. pattern is matched against the events-bus topic (Bun.Glob semantics); title/message may interpolate `{field}` from the event payload, plus the computed `{question}` field (the event payload's first question label, `payload.questions[0].label`, empty string when absent); optional subjectPrefix matches the event payload's subject as a prefix. The optional url is interpolated the same way as title/message and becomes the notification's Open target; a gate rule should set it. A fresh key, not an ownership-latch port, so a default is fine here.",
89
89
  },
90
90
  {
91
91
  key: "rt.cron",
@@ -0,0 +1,18 @@
1
+ /**
2
+ * One field of the machine-local team record, for the write guard. The record
3
+ * itself is owned by repo-tools/lib/team/team-local.ts; this reads only what
4
+ * the guard needs and never writes.
5
+ */
6
+
7
+ import { readFileSync } from "fs";
8
+ import { teamLocalPath } from "./paths.ts";
9
+
10
+ /** Unreadable, absent or malformed all read as false, so nothing that predates the field is refused. */
11
+ export function isJoinedTeam(team: string): boolean {
12
+ try {
13
+ const parsed: unknown = JSON.parse(readFileSync(teamLocalPath(team), "utf8"));
14
+ return typeof parsed === "object" && parsed !== null && (parsed as { joinedByRt?: unknown }).joinedByRt === true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
@@ -100,6 +100,7 @@ import { dirname } from "path";
100
100
  import { machineSettingsPath, teamSettingsPath, userSettingsPath } from "./paths.ts";
101
101
  import { getDef, isMigrated, validateValue, type SettingDef, type SettingScope } from "./registry-machinery.ts";
102
102
  import { listTeams } from "./stores.ts";
103
+ import { isJoinedTeam } from "./team-local-read.ts";
103
104
 
104
105
  export interface SetSettingOpts {
105
106
  /** Normalized repo identity — required to target a repoScoped key's `repos.<identity>` section. */
@@ -208,6 +209,19 @@ function migratedFalseMessage(key: string, def: SettingDef): string {
208
209
  return `"${key}" is not writable through the settings resolver yet${legacyPart}`;
209
210
  }
210
211
 
212
+ /**
213
+ * A clone that arrived by redeeming an invite is pull-only, so a write here
214
+ * would never reach the team AND would leave a tracked file dirty, which is
215
+ * enough on its own to make the daemon's fast-forward pull fail.
216
+ */
217
+ function refuseIfJoined(team: string): void {
218
+ if (isJoinedTeam(team)) {
219
+ refuse(
220
+ `this machine joined "${team}" by invite, so its clone is pull-only and team settings cannot be written here. Ask the team's owner to make this change. Member-proposed changes are tracked in MAT-415.`,
221
+ );
222
+ }
223
+ }
224
+
211
225
  /** Resolves which store file a write targets, applying the team-selection rule for `scope: "team"`. */
212
226
  function resolveStorePath(scope: SettingScope, opts: SetSettingOpts): string {
213
227
  if (scope === "user") return userSettingsPath();
@@ -218,6 +232,7 @@ function resolveStorePath(scope: SettingScope, opts: SetSettingOpts): string {
218
232
  if (!existsSync(path)) {
219
233
  refuse(`team store for "${opts.team}" does not exist (${path}) — clone/seed it before writing to it`);
220
234
  }
235
+ refuseIfJoined(opts.team);
221
236
  return path;
222
237
  }
223
238
 
@@ -228,7 +243,9 @@ function resolveStorePath(scope: SettingScope, opts: SetSettingOpts): string {
228
243
  if (teams.length > 1) {
229
244
  refuse(`multiple local team stores found (${teams.join(", ")}) — pass opts.team to choose one`);
230
245
  }
231
- return teamSettingsPath(teams[0] as string);
246
+ const team = teams[0] as string;
247
+ refuseIfJoined(team);
248
+ return teamSettingsPath(team);
232
249
  }
233
250
 
234
251
  /**
@@ -243,7 +260,9 @@ function resolveStorePathForUnset(scope: SettingScope, opts: SetSettingOpts): st
243
260
 
244
261
  if (opts.team !== undefined) {
245
262
  const path = teamSettingsPath(opts.team);
246
- return existsSync(path) ? path : null;
263
+ if (!existsSync(path)) return null;
264
+ refuseIfJoined(opts.team);
265
+ return path;
247
266
  }
248
267
 
249
268
  const teams = listTeams();
@@ -251,7 +270,9 @@ function resolveStorePathForUnset(scope: SettingScope, opts: SetSettingOpts): st
251
270
  if (teams.length > 1) {
252
271
  refuse(`multiple local team stores found (${teams.join(", ")}) — pass opts.team to choose one`);
253
272
  }
254
- return teamSettingsPath(teams[0] as string);
273
+ const team = teams[0] as string;
274
+ refuseIfJoined(team);
275
+ return teamSettingsPath(team);
255
276
  }
256
277
 
257
278
  /** `// header comment\n{}\n` — see module doc for why the object must be seeded before the first `modify`. */