@mattstack/rt-client 0.19.0 → 0.20.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/README.md CHANGED
@@ -104,6 +104,7 @@ const stop = createRelay({ // one daemon sub
104
104
  | `chatPost` / `chatDm` / `chatDmOpen` / `chatRead` / `chatMessages` / `chatMark` | messages: post, DM, open a DM room without posting, read-and-advance, page, advance the cursor |
105
105
  | `paneList` / `panePeek` / `paneSpawn` / `paneAccounts` / `paneDirectories` | herdr panes: list with presence joined, peek a screen, start claude in a tab, cswap accounts, directory suggestions |
106
106
  | `chatInvite` | type `/chat:join <room>` into a pane; `accepted`, `queued` or `refused` |
107
+ | `paneSend` | type a line into a pane; `accepted`, `queued` or `refused`, plus an optional `continuation` the daemon types once the pane's turn has ended |
107
108
  | `createRelay` / `subscribe` | the event stream; `daemonHealth` the reachability probe |
108
109
 
109
110
  Pass `{ sockPath }` as the trailing options to reach a non-default daemon
package/dist/client.d.ts CHANGED
@@ -183,6 +183,9 @@ export declare function paneSend(a: Commands["pane:send"]["payload"], o?: RtClie
183
183
  /** Brings a herdr pane to the front. The daemon routes this to the tray, which
184
184
  owns the herdr focus and the native terminal-window raise. */
185
185
  export declare function paneFocus(a: Commands["pane:focus"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["pane:focus"]["data"]>>;
186
+ /** Never triggers a sweep; a plain read of the last one's snapshot. */
187
+ export declare function reconcilerStatus(o?: RtClientOptions): Promise<RtResponse<Commands["reconciler:status"]["data"]>>;
188
+ export declare function reconcilerClear(a: Commands["reconciler:clear"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["reconciler:clear"]["data"]>>;
186
189
  export declare function gateOpen(a: Commands["gate:open"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:open"]["data"]>>;
187
190
  export declare function gateAnswer(a: Commands["gate:answer"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["gate:answer"]["data"]>>;
188
191
  /** Daemon clamps its own wait to 240s (gates-store.ts); the client abort
@@ -218,7 +221,10 @@ export declare function herdAttend(a: Commands["herd:attend"]["payload"], o?: Rt
218
221
  export declare function herdWrapUp(a: Commands["herd:wrap-up"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["herd:wrap-up"]["data"]>>;
219
222
  /** Runs `herdr session stop`, one CLI call under the runner's own 15s budget. */
220
223
  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. */
224
+ /** A cold ensure spawns `herdr server`, waits out bg-service's own 10s
225
+ readyTimeoutMs, then runs its parity probes sequentially -- the 15s
226
+ default budget every other rt-client call inherits can lose that race,
227
+ so this is the one wrapper that widens its own. */
222
228
  export declare function bgEnsure(a?: Commands["bg:ensure"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["bg:ensure"]["data"]>>;
223
229
  /** Never ensures/spawns; a plain read of the current state. */
224
230
  export declare function bgStatus(o?: RtClientOptions): Promise<RtResponse<Commands["bg:status"]["data"]>>;
@@ -102,6 +102,8 @@ export interface EventsBusEvent {
102
102
  value gates-store.ts's release tracking (CAS winner or loser) reacts to. */
103
103
  export declare const GATE_BY_PANE = "pane";
104
104
  export type GateStatus = "open" | "answered" | "parked" | "closed";
105
+ /** Reconciler's view of an agent's liveness; also the value `GateRow.executor` is stamped with. */
106
+ export type ExecutorState = "live" | "blocked" | "hidden" | "gone" | "cleared" | "unknown";
105
107
  export type GateOption = string | {
106
108
  value: string;
107
109
  label: string;
@@ -128,6 +130,7 @@ export interface GateAnswer {
128
130
  }>;
129
131
  by: string;
130
132
  answeredAt: number;
133
+ overridden?: boolean;
131
134
  }
132
135
  export interface GateRow {
133
136
  id: string;
@@ -142,17 +145,29 @@ export interface GateRow {
142
145
  openedAt: number;
143
146
  parkedAt: number | null;
144
147
  closedAt: number | null;
145
- closedReason: "abandoned" | "superseded" | "pruned" | null;
148
+ /** "resolved" is stamped only by the reconciler's auto-recovery close
149
+ (a blocked/gone pane came back); every other closer uses one of the
150
+ other three. */
151
+ closedReason: "abandoned" | "superseded" | "pruned" | "resolved" | null;
152
+ /** Set only when `closedReason` is "superseded": the id of the gate that superseded this one. */
153
+ supersededBy: string | null;
146
154
  agent: string | null;
147
155
  pane: string | null;
148
156
  nudge: {
149
157
  session: string;
150
158
  } | null;
151
159
  delivery: {
152
- outcome: "delivered" | "dead-pane";
160
+ outcome: "delivered" | "dead-pane" | "confirmed" | "stuck";
153
161
  at: number;
154
162
  } | null;
155
163
  released: boolean;
164
+ owner: string | null;
165
+ escalatedAt: number | null;
166
+ /** Set by answer-time execution handling: an answered gate whose executor
167
+ couldn't be resumed. Cleared (absent) once resumption succeeds. */
168
+ execution?: "unassigned";
169
+ /** Stamped by the reconciler sweep for open/parked rows only. */
170
+ executor?: ExecutorState;
156
171
  }
157
172
  export interface GateSubscription {
158
173
  id: string;
@@ -164,6 +179,8 @@ export interface GateSubscription {
164
179
  at: number;
165
180
  } | null;
166
181
  dead: boolean;
182
+ scope: "prefix" | "owner";
183
+ ownerRef: string | null;
167
184
  }
168
185
  export interface HerdInfo {
169
186
  id: string;
@@ -206,7 +223,7 @@ export interface HerdStatusData {
206
223
  openGate: string | null;
207
224
  paneStatus: string | null;
208
225
  lastGateStatus: GateStatus | null;
209
- lastGateDelivery: "delivered" | "dead-pane" | null;
226
+ lastGateDelivery: "delivered" | "dead-pane" | "confirmed" | "stuck" | null;
210
227
  }>;
211
228
  unread: number;
212
229
  lifecycleConnected: boolean;
@@ -217,6 +234,11 @@ export interface HerdStatusData {
217
234
  dead: boolean;
218
235
  lastDelivery: GateSubscription["lastDelivery"];
219
236
  } | null;
237
+ /** Whether the shepherd session's own inbox socket is reachable right now, probed fresh on every status call -- honest liveness, not the subscription row's bookkeeping. */
238
+ push: {
239
+ state: "reachable" | "unreachable";
240
+ lastDelivery: GateSubscription["lastDelivery"];
241
+ };
220
242
  }
221
243
  /**
222
244
  * Duplicated shape on purpose, same reasoning as EventsBusEvent above:
@@ -333,10 +355,14 @@ export interface InviteResult {
333
355
  }
334
356
  /** Duplicated shape on purpose: mirrors lib/daemon/inject.ts's InjectResult. */
335
357
  export type PaneDelivery = "accepted" | "queued" | "refused";
358
+ /** `continuation` is present only when the daemon scheduled a second line for after the target's current turn. */
336
359
  export interface PaneSendResult {
337
360
  paneId: string;
338
361
  delivered: PaneDelivery;
339
362
  reason?: string;
363
+ continuation?: {
364
+ delivered: "deferred";
365
+ };
340
366
  }
341
367
  /** `attendTab` is set only for a `bg:` ref: focus for a background pane IS
342
368
  the attend flow (a visible tab running a terminal attach), and this is
@@ -419,6 +445,23 @@ export interface RunDetail {
419
445
  schemaAhead: boolean;
420
446
  }
421
447
  export type AgentSurface = "herdr" | "headless";
448
+ /** The reconciler's per-agent snapshot: one row per agent it knows about, live or not. */
449
+ export interface ExecutorView {
450
+ agentId: string;
451
+ repo: string | null;
452
+ subject: string | null;
453
+ surface: AgentSurface;
454
+ sessionId: string;
455
+ paneRef: string | null;
456
+ state: ExecutorState;
457
+ since: number;
458
+ openGateIds: string[];
459
+ }
460
+ export interface ReconcilerStatus {
461
+ sweptAt: number;
462
+ herdrReachable: boolean;
463
+ executors: ExecutorView[];
464
+ }
422
465
  export interface AgentRecord {
423
466
  id: string;
424
467
  repo: string;
@@ -432,6 +475,12 @@ export interface AgentRecord {
432
475
  label?: string;
433
476
  caller?: string;
434
477
  handle?: string;
478
+ /** The gate-protocol subject stamped as RT_GATE_SUBJECT at launch. Left
479
+ undefined when the caller passed none (RT_GATE_SUBJECT still falls
480
+ back to "agent:<id>" at launch time); an explicit value is persisted
481
+ so a resume re-stamps the same one, AND gates whether the gate-fork
482
+ PreToolUse hook gets injected at all. */
483
+ subject?: string;
435
484
  paneId?: string;
436
485
  tabId?: string;
437
486
  workspaceId?: string;
@@ -616,9 +665,12 @@ export interface WorktreeCreateData {
616
665
  }
617
666
  export interface WorktreeDisposeData {
618
667
  disposed: string[];
668
+ /** `detail` is set only for a refusal whose bare `reason` code can't name
669
+ what a human needs to act on it (the run id and stage for `running-run`). */
619
670
  refused: Array<{
620
671
  tree: string;
621
672
  reason: string;
673
+ detail?: string;
622
674
  }>;
623
675
  recoverable: Array<{
624
676
  tree: string;
@@ -644,6 +696,7 @@ export interface WorktreeAdoptData {
644
696
  refused: Array<{
645
697
  tree: string;
646
698
  reason: string;
699
+ detail?: string;
647
700
  }>;
648
701
  }
649
702
  /** Duplicated shape on purpose: mirrors lib/endpoint/store.ts's EndpointClaim. */
@@ -1080,6 +1133,7 @@ export interface Commands {
1080
1133
  herdrSocket?: string;
1081
1134
  handle?: string;
1082
1135
  bg?: boolean;
1136
+ subject?: string;
1083
1137
  };
1084
1138
  data: AgentRecord;
1085
1139
  };
@@ -1166,6 +1220,7 @@ export interface Commands {
1166
1220
  paneId: string;
1167
1221
  text: string;
1168
1222
  callerPane?: string;
1223
+ continuation?: string;
1169
1224
  };
1170
1225
  data: PaneSendResult;
1171
1226
  };
@@ -1344,6 +1399,20 @@ export interface Commands {
1344
1399
  payload: Record<string, never>;
1345
1400
  data: unknown;
1346
1401
  };
1402
+ /** Read-only: the last sweep's snapshot. Never triggers a sweep itself. */
1403
+ "reconciler:status": {
1404
+ payload: Record<string, never>;
1405
+ data: ReconcilerStatus;
1406
+ };
1407
+ /** Manual override: marks the agent cleared and closes its open/parked gates (reconciler.ts's clear()). */
1408
+ "reconciler:clear": {
1409
+ payload: {
1410
+ agentId: string;
1411
+ };
1412
+ data: {
1413
+ cleared: true;
1414
+ };
1415
+ };
1347
1416
  "gate:open": {
1348
1417
  payload: {
1349
1418
  subject: string;
@@ -1368,12 +1437,21 @@ export interface Commands {
1368
1437
  * `conflict:true` and the WINNING row, so every consumer gets the winner
1369
1438
  * typed with no envelope hacks. `ok:false` is reserved for
1370
1439
  * not-found/closed/validation failures.
1440
+ *
1441
+ * Owner enforcement adds two structured rejections beyond the plain
1442
+ * `{ok:false, error:string}` shape (see `GateAnswerResult` in
1443
+ * `lib/daemon/handlers/gate.ts`): a herd-owned gate answered by anyone but
1444
+ * the owning shepherd's session, the answering pane, or an explicit human
1445
+ * `override` returns `{ok:false, error:"owned-by", owner}`; a closed gate
1446
+ * returns `{ok:false, error:"gate-closed", reason, supersededBy?}`.
1371
1447
  */
1372
1448
  "gate:answer": {
1373
1449
  payload: {
1374
1450
  id: string;
1375
1451
  answers: GateAnswer["answers"];
1376
1452
  by: string;
1453
+ session?: string;
1454
+ override?: boolean;
1377
1455
  };
1378
1456
  data: {
1379
1457
  row: GateRow;
@@ -1430,6 +1508,8 @@ export interface Commands {
1430
1508
  payload: {
1431
1509
  subjectPrefix: string;
1432
1510
  session: string;
1511
+ scope?: "owner";
1512
+ ownerRef?: string;
1433
1513
  };
1434
1514
  data: {
1435
1515
  id: string;
@@ -1505,7 +1585,8 @@ export interface Commands {
1505
1585
  };
1506
1586
  data: {
1507
1587
  job: string;
1508
- status: "closed";
1588
+ status: "closed"; /** Advisory: a resumable run can still write into this job's worktree after close, so this warns rather than blocking. */
1589
+ warning?: string;
1509
1590
  };
1510
1591
  };
1511
1592
  /** `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. */
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, 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";
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, reconcilerStatus, reconcilerClear, 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, HerdInfo, HerdListRow, HerdJobInfo, HerdStatusData, } 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, ExecutorState, ExecutorView, ReconcilerStatus, 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";
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
5
5
  import { homedir } from "os";
6
6
  import { join } from "path";
7
7
  function defaultSock() {
8
- return join(process.env.HOME ?? homedir(), ".mattstack", "rt", "rt.sock");
8
+ return process.env.RT_DAEMON_SOCK || join(process.env.HOME ?? homedir(), ".mattstack", "rt", "rt.sock");
9
9
  }
10
10
  var DEFAULT_SOCK = defaultSock();
11
11
  async function rtCommand(cmd, payload, opts = {}) {
@@ -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", "env", "herdrSocket", "handle", "bg"]) {
208
+ for (const k of ["prompt", "surface", "model", "effort", "account", "label", "caller", "workspace", "tab", "extraArgs", "env", "herdrSocket", "handle", "bg", "subject"]) {
209
209
  if (a[k] !== undefined)
210
210
  payload[k] = a[k];
211
211
  }
@@ -265,6 +265,8 @@ function paneSend(a, o = {}) {
265
265
  const payload = { paneId: a.paneId, text: a.text };
266
266
  if (a.callerPane !== undefined)
267
267
  payload.callerPane = a.callerPane;
268
+ if (a.continuation !== undefined)
269
+ payload.continuation = a.continuation;
268
270
  return rtCommand("pane:send", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30000 });
269
271
  }
270
272
  function paneFocus(a, o = {}) {
@@ -273,6 +275,12 @@ function paneFocus(a, o = {}) {
273
275
  payload.callerWorkspace = a.callerWorkspace;
274
276
  return rtCommand("pane:focus", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
275
277
  }
278
+ function reconcilerStatus(o = {}) {
279
+ return rtCommand("reconciler:status", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
280
+ }
281
+ function reconcilerClear(a, o = {}) {
282
+ return rtCommand("reconciler:clear", { agentId: a.agentId }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
283
+ }
276
284
  function gateOpen(a, o = {}) {
277
285
  const payload = { subject: a.subject, kind: a.kind, questions: a.questions };
278
286
  for (const k of ["meta", "agent", "pane", "nudge", "context", "origin"])
@@ -281,7 +289,11 @@ function gateOpen(a, o = {}) {
281
289
  return rtCommand("gate:open", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
282
290
  }
283
291
  function gateAnswer(a, o = {}) {
284
- return rtCommand("gate:answer", { id: a.id, answers: a.answers, by: a.by }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
292
+ const payload = { id: a.id, answers: a.answers, by: a.by };
293
+ for (const k of ["session", "override"])
294
+ if (a[k] !== undefined)
295
+ payload[k] = a[k];
296
+ return rtCommand("gate:answer", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
285
297
  }
286
298
  function gateWait(a, o = {}) {
287
299
  const payload = { id: a.id };
@@ -303,7 +315,11 @@ function gateClose(a, o = {}) {
303
315
  return rtCommand("gate:close", { id: a.id, reason: a.reason }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
304
316
  }
305
317
  function gateSubscribe(a, o = {}) {
306
- return rtCommand("gate:subscribe", { subjectPrefix: a.subjectPrefix, session: a.session }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
318
+ const payload = { subjectPrefix: a.subjectPrefix, session: a.session };
319
+ for (const k of ["scope", "ownerRef"])
320
+ if (a[k] !== undefined)
321
+ payload[k] = a[k];
322
+ return rtCommand("gate:subscribe", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
307
323
  }
308
324
  function gateUnsubscribe(a, o = {}) {
309
325
  return rtCommand("gate:unsubscribe", { id: a.id }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
@@ -383,7 +399,7 @@ function bgEnsure(a = {}, o = {}) {
383
399
  const payload = {};
384
400
  if (a.claim !== undefined)
385
401
  payload.claim = a.claim;
386
- return rtCommand("bg:ensure", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 15000 });
402
+ return rtCommand("bg:ensure", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30000 });
387
403
  }
388
404
  function bgStatus(o = {}) {
389
405
  return rtCommand("bg:status", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
@@ -470,6 +486,8 @@ var COMMAND_NAMES = [
470
486
  "endpoint:status",
471
487
  "repos:locate",
472
488
  "freshness:reconcile",
489
+ "reconciler:status",
490
+ "reconciler:clear",
473
491
  "gate:open",
474
492
  "gate:answer",
475
493
  "gate:wait",
@@ -705,6 +723,9 @@ function formatPaneRef(paneId, server) {
705
723
  if (server === "visible") {
706
724
  return paneId;
707
725
  }
726
+ if (paneId.startsWith(BG_PREFIX)) {
727
+ return paneId;
728
+ }
708
729
  return BG_PREFIX + paneId;
709
730
  }
710
731
  // src/settings/resolve.ts
@@ -777,7 +798,7 @@ var REGISTRY = [
777
798
  merge: "deep",
778
799
  repoScoped: true,
779
800
  migrated: true,
780
- description: "Per-repo worktree pool config (onDeck size, ready steps, name pool); root/branchFormat/ready computed-or-empty in the reader."
801
+ description: "Per-repo worktree pool config (onDeck size, ready steps, name pool, staleClaimDays); root/branchFormat/ready computed-or-empty in the reader."
781
802
  },
782
803
  {
783
804
  key: "rt.worktreeReadyApproval",
@@ -819,7 +840,7 @@ var REGISTRY = [
819
840
  scopes: ["user"],
820
841
  default: [],
821
842
  merge: "replace",
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."
843
+ description: 'Event-bus glob rules that raise a desktop notification: [{pattern, category, title, message, subjectPrefix?, url?, owner?}]. 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. The optional owner field (only literal "human" is valid) suppresses events whose payload.owner starts with "herd:", allowing gate rules to skip herd-owned events. A typical setup pairs a gate/opened rule with owner: "human" (human-owned gates notify) and a gate/escalated rule without owner (all escalations notify, whether human or herd). A fresh key, not an ownership-latch port, so a default is fine here.'
823
844
  },
824
845
  {
825
846
  key: "rt.cron",
@@ -1020,6 +1041,14 @@ var REGISTRY = [
1020
1041
  merge: "replace",
1021
1042
  description: 'The machine\'s intended flavor, "dev" or "prod". Normally written by `rt settings dev-mode` after a successful handoff; a manual `rt settings set` is the blessed repair escape hatch — the daemon park loop converges on whatever this says. Unset ⇒ derived from the dev wrapper\'s presence.'
1022
1043
  },
1044
+ {
1045
+ key: "setup.waived",
1046
+ type: "array",
1047
+ scopes: ["machine"],
1048
+ default: [],
1049
+ merge: "replace",
1050
+ description: "Finish-gated setup rows the user skipped on this Mac through `rt setup waive` (today only tool.fast-browser-extension); the wizard's Finish no longer waits on them. Machine-only: a loaded Chrome extension is a per-profile fact and the choice is per machine, so it never travels with a team or user store."
1051
+ },
1023
1052
  {
1024
1053
  key: "rt.integrations",
1025
1054
  type: "object",
@@ -1138,7 +1167,7 @@ var REGISTRY = [
1138
1167
  type: "array",
1139
1168
  scopes: ["team"],
1140
1169
  merge: "replace",
1141
- description: "Board tabs ({id, label, source, slackChannel?, reviewSkill?}), editable from the board's settings modal. source.kind 'authors' is the classic roster board; 'codeowners' lists MRs from any author blocked on an unapproved CODEOWNERS section. Absent = one implicit authors tab (fallback lives in the board reader, never here)."
1170
+ description: "Board tabs ({id, label, source, slackChannel?, reviewSkill?}), editable from the board's settings modal. source.kind 'authors' is the classic roster board; 'codeowners' lists any author's open MRs carrying a CODE_OWNER rule for the section, until merge/close. Absent = one implicit authors tab (fallback lives in the board reader, never here)."
1142
1171
  },
1143
1172
  {
1144
1173
  key: "board.staleAfterDays",
@@ -1381,6 +1410,14 @@ var REGISTRY = [
1381
1410
  scopes: ["user", "machine"],
1382
1411
  merge: "replace",
1383
1412
  description: "Opaque extra claude arguments appended to every rt agent launch (escape hatch)."
1413
+ },
1414
+ {
1415
+ key: "rt.gates.escalationTtlMinutes",
1416
+ type: "number",
1417
+ scopes: ["user"],
1418
+ default: 10,
1419
+ merge: "replace",
1420
+ description: `Minutes an open herd-owned gate waits before the escalation sweep surfaces it to the human (topic gate/escalated/<id>). Fires on either trigger: the TTL elapses (reason "ttl"), or the owning herd's shepherd subscription is gone or dead before the TTL (reason "owner-dead"). 0 escalates any eligible gate on the first sweep after it opens. A fresh key, not an ownership-latch port, so a default is fine here.`
1384
1421
  }
1385
1422
  ];
1386
1423
 
@@ -2234,6 +2271,8 @@ export {
2234
2271
  resolveNameToIdentity,
2235
2272
  resolveForgeToken,
2236
2273
  repoNameForPath,
2274
+ reconcilerStatus,
2275
+ reconcilerClear,
2237
2276
  readStore,
2238
2277
  readProjectMRs,
2239
2278
  readMrsByBranch,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mattstack/rt-client",
3
- "version": "0.19.0",
3
+ "version": "0.20.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", "env", "herdrSocket", "handle", "bg"] as const) {
359
+ for (const k of ["prompt", "surface", "model", "effort", "account", "label", "caller", "workspace", "tab", "extraArgs", "env", "herdrSocket", "handle", "bg", "subject"] 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 });
@@ -432,6 +432,7 @@ export function paneSend(
432
432
  ): Promise<RtResponse<Commands["pane:send"]["data"]>> {
433
433
  const payload: Record<string, unknown> = { paneId: a.paneId, text: a.text };
434
434
  if (a.callerPane !== undefined) payload.callerPane = a.callerPane;
435
+ if (a.continuation !== undefined) payload.continuation = a.continuation;
435
436
  return rtCommand<Commands["pane:send"]["data"]>("pane:send", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30_000 });
436
437
  }
437
438
 
@@ -446,6 +447,20 @@ export function paneFocus(
446
447
  return rtCommand<Commands["pane:focus"]["data"]>("pane:focus", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
447
448
  }
448
449
 
450
+ // ─── Reconciler (executor state; lib/daemon/reconciler.ts) ────────────────
451
+
452
+ /** Never triggers a sweep; a plain read of the last one's snapshot. */
453
+ export function reconcilerStatus(o: RtClientOptions = {}): Promise<RtResponse<Commands["reconciler:status"]["data"]>> {
454
+ return rtCommand<Commands["reconciler:status"]["data"]>("reconciler:status", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
455
+ }
456
+
457
+ export function reconcilerClear(
458
+ a: Commands["reconciler:clear"]["payload"],
459
+ o: RtClientOptions = {},
460
+ ): Promise<RtResponse<Commands["reconciler:clear"]["data"]>> {
461
+ return rtCommand<Commands["reconciler:clear"]["data"]>("reconciler:clear", { agentId: a.agentId }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
462
+ }
463
+
449
464
  // ─── Gates (BOARD-20/21 gate facility) ─────────────────────────────────────
450
465
 
451
466
  export function gateOpen(
@@ -461,7 +476,9 @@ export function gateAnswer(
461
476
  a: Commands["gate:answer"]["payload"],
462
477
  o: RtClientOptions = {},
463
478
  ): Promise<RtResponse<Commands["gate:answer"]["data"]>> {
464
- 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 });
479
+ const payload: Record<string, unknown> = { id: a.id, answers: a.answers, by: a.by };
480
+ for (const k of ["session", "override"] as const) if (a[k] !== undefined) payload[k] = a[k];
481
+ return rtCommand<Commands["gate:answer"]["data"]>("gate:answer", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
465
482
  }
466
483
 
467
484
  /** Daemon clamps its own wait to 240s (gates-store.ts); the client abort
@@ -503,7 +520,9 @@ export function gateSubscribe(
503
520
  a: Commands["gate:subscribe"]["payload"],
504
521
  o: RtClientOptions = {},
505
522
  ): Promise<RtResponse<Commands["gate:subscribe"]["data"]>> {
506
- return rtCommand<Commands["gate:subscribe"]["data"]>("gate:subscribe", { subjectPrefix: a.subjectPrefix, session: a.session }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
523
+ const payload: Record<string, unknown> = { subjectPrefix: a.subjectPrefix, session: a.session };
524
+ for (const k of ["scope", "ownerRef"] as const) if (a[k] !== undefined) payload[k] = a[k];
525
+ return rtCommand<Commands["gate:subscribe"]["data"]>("gate:subscribe", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
507
526
  }
508
527
 
509
528
  export function gateUnsubscribe(
@@ -645,14 +664,17 @@ export function herdStopHidden(
645
664
 
646
665
  // ─── Background server (daemon-owned background herdr session) ────────────
647
666
 
648
- /** May spawn `herdr server` and wait for it to bind; budget matches bg-service's own 10s readyTimeoutMs plus margin. */
667
+ /** A cold ensure spawns `herdr server`, waits out bg-service's own 10s
668
+ readyTimeoutMs, then runs its parity probes sequentially -- the 15s
669
+ default budget every other rt-client call inherits can lose that race,
670
+ so this is the one wrapper that widens its own. */
649
671
  export function bgEnsure(
650
672
  a: Commands["bg:ensure"]["payload"] = {},
651
673
  o: RtClientOptions = {},
652
674
  ): Promise<RtResponse<Commands["bg:ensure"]["data"]>> {
653
675
  const payload: Record<string, unknown> = {};
654
676
  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 });
677
+ return rtCommand<Commands["bg:ensure"]["data"]>("bg:ensure", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30_000 });
656
678
  }
657
679
 
658
680
  /** Never ensures/spawns; a plain read of the current state. */
package/src/commands.ts CHANGED
@@ -95,6 +95,8 @@ export interface EventsBusEvent { id: number; topic: string; payload: unknown; e
95
95
  export const GATE_BY_PANE = "pane";
96
96
 
97
97
  export type GateStatus = "open" | "answered" | "parked" | "closed";
98
+ /** Reconciler's view of an agent's liveness; also the value `GateRow.executor` is stamped with. */
99
+ export type ExecutorState = "live" | "blocked" | "hidden" | "gone" | "cleared" | "unknown";
98
100
  export type GateOption = string | { value: string; label: string };
99
101
  export interface GateOrigin {
100
102
  paneId?: string;
@@ -110,7 +112,7 @@ export function gateOptionValue(o: GateOption): string {
110
112
  export function gateOptionLabel(o: GateOption): string {
111
113
  return typeof o === "string" ? o : (o.label || o.value);
112
114
  }
113
- export interface GateAnswer { answers: Record<string, string | string[] | { value: string | string[]; note?: string }>; by: string; answeredAt: number }
115
+ export interface GateAnswer { answers: Record<string, string | string[] | { value: string | string[]; note?: string }>; by: string; answeredAt: number; overridden?: boolean }
114
116
  export interface GateRow {
115
117
  id: string; subject: string; kind: string;
116
118
  questions: GateQuestion[]; meta: Record<string, unknown> | null;
@@ -118,11 +120,23 @@ export interface GateRow {
118
120
  origin?: GateOrigin | null;
119
121
  status: GateStatus; answer: GateAnswer | null;
120
122
  openedAt: number; parkedAt: number | null; closedAt: number | null;
121
- closedReason: "abandoned" | "superseded" | "pruned" | null;
123
+ /** "resolved" is stamped only by the reconciler's auto-recovery close
124
+ (a blocked/gone pane came back); every other closer uses one of the
125
+ other three. */
126
+ closedReason: "abandoned" | "superseded" | "pruned" | "resolved" | null;
127
+ /** Set only when `closedReason` is "superseded": the id of the gate that superseded this one. */
128
+ supersededBy: string | null;
122
129
  agent: string | null; pane: string | null;
123
130
  nudge: { session: string } | null;
124
- delivery: { outcome: "delivered" | "dead-pane"; at: number } | null;
131
+ delivery: { outcome: "delivered" | "dead-pane" | "confirmed" | "stuck"; at: number } | null;
125
132
  released: boolean;
133
+ owner: string | null;
134
+ escalatedAt: number | null;
135
+ /** Set by answer-time execution handling: an answered gate whose executor
136
+ couldn't be resumed. Cleared (absent) once resumption succeeds. */
137
+ execution?: "unassigned";
138
+ /** Stamped by the reconciler sweep for open/parked rows only. */
139
+ executor?: ExecutorState;
126
140
  }
127
141
 
128
142
  export interface GateSubscription {
@@ -132,6 +146,8 @@ export interface GateSubscription {
132
146
  createdAt: number;
133
147
  lastDelivery: { outcome: "delivered" | "failed"; at: number } | null;
134
148
  dead: boolean;
149
+ scope: "prefix" | "owner";
150
+ ownerRef: string | null;
135
151
  }
136
152
 
137
153
  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 }
@@ -141,12 +157,14 @@ export interface HerdJobInfo { herd: string; name: string; worktree: string; bra
141
157
  /** `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
158
  export interface HerdStatusData {
143
159
  herd: HerdInfo;
144
- jobs: Array<HerdJobInfo & { openGate: string | null; paneStatus: string | null; lastGateStatus: GateStatus | null; lastGateDelivery: "delivered" | "dead-pane" | null }>;
160
+ jobs: Array<HerdJobInfo & { openGate: string | null; paneStatus: string | null; lastGateStatus: GateStatus | null; lastGateDelivery: "delivered" | "dead-pane" | "confirmed" | "stuck" | null }>;
145
161
  unread: number;
146
162
  lifecycleConnected: boolean;
147
163
  hiddenUp: boolean | null;
148
164
  /** The shepherd session's own `herd:<id>/` subscription row, or null when none is live. */
149
165
  subscription: { id: string; dead: boolean; lastDelivery: GateSubscription["lastDelivery"] } | null;
166
+ /** Whether the shepherd session's own inbox socket is reachable right now, probed fresh on every status call -- honest liveness, not the subscription row's bookkeeping. */
167
+ push: { state: "reachable" | "unreachable"; lastDelivery: GateSubscription["lastDelivery"] };
150
168
  }
151
169
 
152
170
  /**
@@ -243,7 +261,8 @@ export interface InviteResult { paneId: string; delivered: "accepted" | "queued"
243
261
 
244
262
  /** Duplicated shape on purpose: mirrors lib/daemon/inject.ts's InjectResult. */
245
263
  export type PaneDelivery = "accepted" | "queued" | "refused";
246
- export interface PaneSendResult { paneId: string; delivered: PaneDelivery; reason?: string }
264
+ /** `continuation` is present only when the daemon scheduled a second line for after the target's current turn. */
265
+ export interface PaneSendResult { paneId: string; delivered: PaneDelivery; reason?: string; continuation?: { delivered: "deferred" } }
247
266
  /** `attendTab` is set only for a `bg:` ref: focus for a background pane IS
248
267
  the attend flow (a visible tab running a terminal attach), and this is
249
268
  that tab's id. */
@@ -297,11 +316,36 @@ export interface RunDetail { run: RunSummary; stages: RunStageRow[]; fields: Run
297
316
 
298
317
  export type AgentSurface = "herdr" | "headless";
299
318
 
319
+ /** The reconciler's per-agent snapshot: one row per agent it knows about, live or not. */
320
+ export interface ExecutorView {
321
+ agentId: string;
322
+ repo: string | null;
323
+ subject: string | null;
324
+ surface: AgentSurface;
325
+ sessionId: string;
326
+ paneRef: string | null;
327
+ state: ExecutorState;
328
+ since: number;
329
+ openGateIds: string[];
330
+ }
331
+
332
+ export interface ReconcilerStatus {
333
+ sweptAt: number;
334
+ herdrReachable: boolean;
335
+ executors: ExecutorView[];
336
+ }
337
+
300
338
  export interface AgentRecord {
301
339
  id: string; repo: string; cwd: string; provider: string;
302
340
  surface: AgentSurface; sessionId: string;
303
341
  model?: string; effort?: string; account?: string;
304
342
  label?: string; caller?: string; handle?: string;
343
+ /** The gate-protocol subject stamped as RT_GATE_SUBJECT at launch. Left
344
+ undefined when the caller passed none (RT_GATE_SUBJECT still falls
345
+ back to "agent:<id>" at launch time); an explicit value is persisted
346
+ so a resume re-stamps the same one, AND gates whether the gate-fork
347
+ PreToolUse hook gets injected at all. */
348
+ subject?: string;
305
349
  paneId?: string; tabId?: string; workspaceId?: string;
306
350
  extraArgs?: string; exitCode?: number; resultPath?: string;
307
351
  createdAt: number; lastResumedAt?: number; finishedAt?: number;
@@ -405,7 +449,9 @@ export interface WorktreeProvisionData {
405
449
  export interface WorktreeCreateData { tree: string; path: string }
406
450
  export interface WorktreeDisposeData {
407
451
  disposed: string[];
408
- refused: Array<{ tree: string; reason: string }>;
452
+ /** `detail` is set only for a refusal whose bare `reason` code can't name
453
+ what a human needs to act on it (the run id and stage for `running-run`). */
454
+ refused: Array<{ tree: string; reason: string; detail?: string }>;
409
455
  recoverable: Array<{ tree: string; path: string; until: string }>;
410
456
  }
411
457
  export interface WorktreeRestoreData {
@@ -414,7 +460,7 @@ export interface WorktreeRestoreData {
414
460
  export interface WorktreeFreshenData { ran: string[] }
415
461
  export interface WorktreeAdoptData {
416
462
  main: string; claimed: string[]; unmanaged: string[]; disposed: string[];
417
- refused: Array<{ tree: string; reason: string }>;
463
+ refused: Array<{ tree: string; reason: string; detail?: string }>;
418
464
  }
419
465
 
420
466
  /** Duplicated shape on purpose: mirrors lib/endpoint/store.ts's EndpointClaim. */
@@ -558,7 +604,7 @@ export interface Commands {
558
604
  "chat:dm-open": { payload: { from: string; to: string; sessionId?: string }; data: { room: string; created: boolean } };
559
605
 
560
606
  // ─── Agent handoff (rt agent) ────────────────────────────────────────────
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 };
607
+ "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; subject?: string }; data: AgentRecord };
562
608
  "agent:resume": { payload: { id: string; prompt?: string; surface?: AgentSurface; workspace?: string; tab?: string }; data: AgentRecord };
563
609
  "agent:get": { payload: { id: string }; data: AgentRecord };
564
610
  "agent:list": { payload: { repo?: string }; data: { agents: AgentRecord[] } };
@@ -571,7 +617,7 @@ export interface Commands {
571
617
  payload: { cwd: string; account?: string; model?: string; effort?: string; prompt?: string; workspace?: string };
572
618
  data: { pane: ChatPane; ready: boolean };
573
619
  };
574
- "pane:send": { payload: { paneId: string; text: string; callerPane?: string }; data: PaneSendResult };
620
+ "pane:send": { payload: { paneId: string; text: string; callerPane?: string; continuation?: string }; data: PaneSendResult };
575
621
  /** `callerWorkspace` (HERDR_WORKSPACE_ID) is required only for a `bg:`
576
622
  ref, whose focus opens an attend tab in the caller's own workspace. */
577
623
  "pane:focus": { payload: { paneId: string; callerWorkspace?: string }; data: PaneFocusResult };
@@ -609,6 +655,12 @@ export interface Commands {
609
655
  "repos:locate": { payload: { newPath: string; repo?: string; dryRun?: boolean }; data: unknown };
610
656
  "freshness:reconcile": { payload: Record<string, never>; data: unknown };
611
657
 
658
+ // ─── Reconciler (executor state; lib/daemon/reconciler.ts) ───────────────
659
+ /** Read-only: the last sweep's snapshot. Never triggers a sweep itself. */
660
+ "reconciler:status": { payload: Record<string, never>; data: ReconcilerStatus };
661
+ /** Manual override: marks the agent cleared and closes its open/parked gates (reconciler.ts's clear()). */
662
+ "reconciler:clear": { payload: { agentId: string }; data: { cleared: true } };
663
+
612
664
  // ─── Gate facility (BOARD-20/21) ─────────────────────────────────────────
613
665
  "gate:open": { payload: { subject: string; kind: string; questions: GateQuestion[]; meta?: Record<string, unknown>; agent?: string; pane?: string; nudge?: { session: string }; context?: string; origin?: GateOrigin }; data: { id: string; supersededId: string | null } };
614
666
  /**
@@ -616,8 +668,15 @@ export interface Commands {
616
668
  * `conflict:true` and the WINNING row, so every consumer gets the winner
617
669
  * typed with no envelope hacks. `ok:false` is reserved for
618
670
  * not-found/closed/validation failures.
671
+ *
672
+ * Owner enforcement adds two structured rejections beyond the plain
673
+ * `{ok:false, error:string}` shape (see `GateAnswerResult` in
674
+ * `lib/daemon/handlers/gate.ts`): a herd-owned gate answered by anyone but
675
+ * the owning shepherd's session, the answering pane, or an explicit human
676
+ * `override` returns `{ok:false, error:"owned-by", owner}`; a closed gate
677
+ * returns `{ok:false, error:"gate-closed", reason, supersededBy?}`.
619
678
  */
620
- "gate:answer": { payload: { id: string; answers: GateAnswer["answers"]; by: string }; data: { row: GateRow; conflict?: true } };
679
+ "gate:answer": { payload: { id: string; answers: GateAnswer["answers"]; by: string; session?: string; override?: boolean }; data: { row: GateRow; conflict?: true } };
621
680
  /** `ok:false "not-found"` on an unknown id is terminal; the CLI loop must not re-enter on it.
622
681
  * `timeout` carries no row (nothing settled); `answered`/`closed` always carry the settled row. */
623
682
  "gate:wait": { payload: { id: string; waitMs?: number }; data: { status: "timeout" } | { status: "answered" | "closed"; row: GateRow } };
@@ -626,7 +685,7 @@ export interface Commands {
626
685
  "gate:list": { payload: { open?: boolean; subjectPrefix?: string; kind?: string; limit?: number; cursor?: number }; data: { gates: GateRow[]; cursor: number } };
627
686
  "gate:park": { payload: { id: string }; data: { ok: true } };
628
687
  "gate:close": { payload: { id: string; reason: "abandoned" | "superseded" | "pruned" }; data: { ok: true } };
629
- "gate:subscribe": { payload: { subjectPrefix: string; session: string }; data: { id: string } };
688
+ "gate:subscribe": { payload: { subjectPrefix: string; session: string; scope?: "owner"; ownerRef?: string }; data: { id: string } };
630
689
  "gate:unsubscribe": { payload: { id: string }; data: { removed: boolean } };
631
690
  /** The shepherd's gap-recovery liveness check and the observability window
632
691
  * onto delivery outcomes (dead marks included). */
@@ -638,7 +697,7 @@ export interface Commands {
638
697
  "herd:status": { payload: { herd: string }; data: HerdStatusData };
639
698
  /** Active herds only unless `all`, so a shepherd's "which herd am I on" question has one answer. */
640
699
  "herd:list": { payload: { all?: boolean }; data: { herds: HerdListRow[] } };
641
- "herd:close": { payload: { herd: string; job: string }; data: { job: string; status: "closed" } };
700
+ "herd:close": { payload: { herd: string; job: string }; data: { job: string; status: "closed"; /** Advisory: a resumable run can still write into this job's worktree after close, so this warns rather than blocking. */ warning?: string } };
642
701
  /** `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
702
  "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
703
  "herd:gates": { payload: { herd: string }; data: { gates: GateRow[] } };
@@ -752,6 +811,8 @@ export const COMMAND_NAMES: readonly CommandName[] = [
752
811
  "endpoint:status",
753
812
  "repos:locate",
754
813
  "freshness:reconcile",
814
+ "reconciler:status",
815
+ "reconciler:clear",
755
816
  "gate:open",
756
817
  "gate:answer",
757
818
  "gate:wait",
package/src/index.ts CHANGED
@@ -45,6 +45,8 @@ export {
45
45
  chatInvite,
46
46
  paneSend,
47
47
  paneFocus,
48
+ reconcilerStatus,
49
+ reconcilerClear,
48
50
  gateOpen,
49
51
  gateAnswer,
50
52
  gateWait,
@@ -104,6 +106,9 @@ export type {
104
106
  AgentRecord,
105
107
  AgentSurface,
106
108
  AgentStatus,
109
+ ExecutorState,
110
+ ExecutorView,
111
+ ReconcilerStatus,
107
112
  ChatPane,
108
113
  PaneAccount,
109
114
  PaneDirectory,
package/src/pane-ref.ts CHANGED
@@ -24,5 +24,10 @@ export function formatPaneRef(paneId: string, server: PaneServer): string {
24
24
  if (server === "visible") {
25
25
  return paneId;
26
26
  }
27
+ // Idempotent: herdr pane ids (w<N>:p<N>) can never start with "bg:", so an
28
+ // already-formatted ref passes through unchanged instead of nesting.
29
+ if (paneId.startsWith(BG_PREFIX)) {
30
+ return paneId;
31
+ }
27
32
  return BG_PREFIX + paneId;
28
33
  }
@@ -42,7 +42,7 @@ export const REGISTRY: readonly SettingDef[] = [
42
42
  merge: "deep",
43
43
  repoScoped: true,
44
44
  migrated: true,
45
- description: "Per-repo worktree pool config (onDeck size, ready steps, name pool); root/branchFormat/ready computed-or-empty in the reader.",
45
+ description: "Per-repo worktree pool config (onDeck size, ready steps, name pool, staleClaimDays); root/branchFormat/ready computed-or-empty in the reader.",
46
46
  },
47
47
  {
48
48
  key: "rt.worktreeReadyApproval",
@@ -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, 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.",
88
+ description: "Event-bus glob rules that raise a desktop notification: [{pattern, category, title, message, subjectPrefix?, url?, owner?}]. 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. The optional owner field (only literal \"human\" is valid) suppresses events whose payload.owner starts with \"herd:\", allowing gate rules to skip herd-owned events. A typical setup pairs a gate/opened rule with owner: \"human\" (human-owned gates notify) and a gate/escalated rule without owner (all escalations notify, whether human or herd). A fresh key, not an ownership-latch port, so a default is fine here.",
89
89
  },
90
90
  {
91
91
  key: "rt.cron",
@@ -295,6 +295,15 @@ export const REGISTRY: readonly SettingDef[] = [
295
295
  merge: "replace",
296
296
  description: "The machine's intended flavor, \"dev\" or \"prod\". Normally written by `rt settings dev-mode` after a successful handoff; a manual `rt settings set` is the blessed repair escape hatch — the daemon park loop converges on whatever this says. Unset ⇒ derived from the dev wrapper's presence.",
297
297
  },
298
+ {
299
+ key: "setup.waived",
300
+ type: "array",
301
+ scopes: ["machine"],
302
+ default: [],
303
+ merge: "replace",
304
+ description:
305
+ "Finish-gated setup rows the user skipped on this Mac through `rt setup waive` (today only tool.fast-browser-extension); the wizard's Finish no longer waits on them. Machine-only: a loaded Chrome extension is a per-profile fact and the choice is per machine, so it never travels with a team or user store.",
306
+ },
298
307
  {
299
308
  key: "rt.integrations",
300
309
  type: "object",
@@ -427,7 +436,7 @@ export const REGISTRY: readonly SettingDef[] = [
427
436
  type: "array",
428
437
  scopes: ["team"],
429
438
  merge: "replace",
430
- description: "Board tabs ({id, label, source, slackChannel?, reviewSkill?}), editable from the board's settings modal. source.kind 'authors' is the classic roster board; 'codeowners' lists MRs from any author blocked on an unapproved CODEOWNERS section. Absent = one implicit authors tab (fallback lives in the board reader, never here).",
439
+ description: "Board tabs ({id, label, source, slackChannel?, reviewSkill?}), editable from the board's settings modal. source.kind 'authors' is the classic roster board; 'codeowners' lists any author's open MRs carrying a CODE_OWNER rule for the section, until merge/close. Absent = one implicit authors tab (fallback lives in the board reader, never here).",
431
440
  },
432
441
 
433
442
  // --- board (user) ----------------------------------------------------------
@@ -687,4 +696,14 @@ export const REGISTRY: readonly SettingDef[] = [
687
696
  merge: "replace",
688
697
  description: "Opaque extra claude arguments appended to every rt agent launch (escape hatch).",
689
698
  },
699
+
700
+ // --- gates (escalation) ----------------------------------------------------
701
+ {
702
+ key: "rt.gates.escalationTtlMinutes",
703
+ type: "number",
704
+ scopes: ["user"],
705
+ default: 10,
706
+ merge: "replace",
707
+ description: "Minutes an open herd-owned gate waits before the escalation sweep surfaces it to the human (topic gate/escalated/<id>). Fires on either trigger: the TTL elapses (reason \"ttl\"), or the owning herd's shepherd subscription is gone or dead before the TTL (reason \"owner-dead\"). 0 escalates any eligible gate on the first sweep after it opens. A fresh key, not an ownership-latch port, so a default is fine here.",
708
+ },
690
709
  ];
package/src/transport.ts CHANGED
@@ -38,8 +38,13 @@ export interface RtClientOptions {
38
38
  // lib/, so this literal cannot import rtDir(). repo-tools/lib/rt-paths.ts is
39
39
  // the authority — change there first, mirror here (same convention as
40
40
  // settings/paths.ts's call-time `home()`).
41
+ //
42
+ // RT_DAEMON_SOCK wins over the HOME-derived default (same pattern as
43
+ // lib/daemon-client.ts's RT_APP_SOCKET for tray.sock): an isolated daemon
44
+ // (per-agent launch, an e2e run) points every rt-client caller, including
45
+ // the `rt` CLI's own subcommands, at its own socket without repointing HOME.
41
46
  function defaultSock(): string {
42
- return join(process.env.HOME ?? homedir(), ".mattstack", "rt", "rt.sock");
47
+ return process.env.RT_DAEMON_SOCK || join(process.env.HOME ?? homedir(), ".mattstack", "rt", "rt.sock");
43
48
  }
44
49
 
45
50
  /**