@mattstack/rt-client 0.19.1 → 0.21.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
@@ -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;
@@ -111,6 +113,7 @@ export interface GateOrigin {
111
113
  tabId?: string;
112
114
  runId?: string;
113
115
  worktree?: string;
116
+ surface?: string;
114
117
  presentation?: "form" | "wait";
115
118
  }
116
119
  export interface GateQuestion {
@@ -143,7 +146,10 @@ export interface GateRow {
143
146
  openedAt: number;
144
147
  parkedAt: number | null;
145
148
  closedAt: number | null;
146
- closedReason: "abandoned" | "superseded" | "pruned" | null;
149
+ /** "resolved" is stamped only by the reconciler's auto-recovery close
150
+ (a blocked/gone pane came back); every other closer uses one of the
151
+ other three. */
152
+ closedReason: "abandoned" | "superseded" | "pruned" | "resolved" | null;
147
153
  /** Set only when `closedReason` is "superseded": the id of the gate that superseded this one. */
148
154
  supersededBy: string | null;
149
155
  agent: string | null;
@@ -152,12 +158,17 @@ export interface GateRow {
152
158
  session: string;
153
159
  } | null;
154
160
  delivery: {
155
- outcome: "delivered" | "dead-pane";
161
+ outcome: "delivered" | "dead-pane" | "confirmed" | "stuck";
156
162
  at: number;
157
163
  } | null;
158
164
  released: boolean;
159
165
  owner: string | null;
160
166
  escalatedAt: number | null;
167
+ /** Set by answer-time execution handling: an answered gate whose executor
168
+ couldn't be resumed. Cleared (absent) once resumption succeeds. */
169
+ execution?: "unassigned";
170
+ /** Stamped by the reconciler sweep for open/parked rows only. */
171
+ executor?: ExecutorState;
161
172
  }
162
173
  export interface GateSubscription {
163
174
  id: string;
@@ -213,7 +224,7 @@ export interface HerdStatusData {
213
224
  openGate: string | null;
214
225
  paneStatus: string | null;
215
226
  lastGateStatus: GateStatus | null;
216
- lastGateDelivery: "delivered" | "dead-pane" | null;
227
+ lastGateDelivery: "delivered" | "dead-pane" | "confirmed" | "stuck" | null;
217
228
  }>;
218
229
  unread: number;
219
230
  lifecycleConnected: boolean;
@@ -345,10 +356,14 @@ export interface InviteResult {
345
356
  }
346
357
  /** Duplicated shape on purpose: mirrors lib/daemon/inject.ts's InjectResult. */
347
358
  export type PaneDelivery = "accepted" | "queued" | "refused";
359
+ /** `continuation` is present only when the daemon scheduled a second line for after the target's current turn. */
348
360
  export interface PaneSendResult {
349
361
  paneId: string;
350
362
  delivered: PaneDelivery;
351
363
  reason?: string;
364
+ continuation?: {
365
+ delivered: "deferred";
366
+ };
352
367
  }
353
368
  /** `attendTab` is set only for a `bg:` ref: focus for a background pane IS
354
369
  the attend flow (a visible tab running a terminal attach), and this is
@@ -431,6 +446,23 @@ export interface RunDetail {
431
446
  schemaAhead: boolean;
432
447
  }
433
448
  export type AgentSurface = "herdr" | "headless";
449
+ /** The reconciler's per-agent snapshot: one row per agent it knows about, live or not. */
450
+ export interface ExecutorView {
451
+ agentId: string;
452
+ repo: string | null;
453
+ subject: string | null;
454
+ surface: AgentSurface;
455
+ sessionId: string;
456
+ paneRef: string | null;
457
+ state: ExecutorState;
458
+ since: number;
459
+ openGateIds: string[];
460
+ }
461
+ export interface ReconcilerStatus {
462
+ sweptAt: number;
463
+ herdrReachable: boolean;
464
+ executors: ExecutorView[];
465
+ }
434
466
  export interface AgentRecord {
435
467
  id: string;
436
468
  repo: string;
@@ -444,6 +476,12 @@ export interface AgentRecord {
444
476
  label?: string;
445
477
  caller?: string;
446
478
  handle?: string;
479
+ /** The gate-protocol subject stamped as RT_GATE_SUBJECT at launch. Left
480
+ undefined when the caller passed none (RT_GATE_SUBJECT still falls
481
+ back to "agent:<id>" at launch time); an explicit value is persisted
482
+ so a resume re-stamps the same one, AND gates whether the gate-fork
483
+ PreToolUse hook gets injected at all. */
484
+ subject?: string;
447
485
  paneId?: string;
448
486
  tabId?: string;
449
487
  workspaceId?: string;
@@ -1096,6 +1134,7 @@ export interface Commands {
1096
1134
  herdrSocket?: string;
1097
1135
  handle?: string;
1098
1136
  bg?: boolean;
1137
+ subject?: string;
1099
1138
  };
1100
1139
  data: AgentRecord;
1101
1140
  };
@@ -1182,6 +1221,7 @@ export interface Commands {
1182
1221
  paneId: string;
1183
1222
  text: string;
1184
1223
  callerPane?: string;
1224
+ continuation?: string;
1185
1225
  };
1186
1226
  data: PaneSendResult;
1187
1227
  };
@@ -1360,6 +1400,20 @@ export interface Commands {
1360
1400
  payload: Record<string, never>;
1361
1401
  data: unknown;
1362
1402
  };
1403
+ /** Read-only: the last sweep's snapshot. Never triggers a sweep itself. */
1404
+ "reconciler:status": {
1405
+ payload: Record<string, never>;
1406
+ data: ReconcilerStatus;
1407
+ };
1408
+ /** Manual override: marks the agent cleared and closes its open/parked gates (reconciler.ts's clear()). */
1409
+ "reconciler:clear": {
1410
+ payload: {
1411
+ agentId: string;
1412
+ };
1413
+ data: {
1414
+ cleared: true;
1415
+ };
1416
+ };
1363
1417
  "gate:open": {
1364
1418
  payload: {
1365
1419
  subject: string;
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"])
@@ -478,6 +486,8 @@ var COMMAND_NAMES = [
478
486
  "endpoint:status",
479
487
  "repos:locate",
480
488
  "freshness:reconcile",
489
+ "reconciler:status",
490
+ "reconciler:clear",
481
491
  "gate:open",
482
492
  "gate:answer",
483
493
  "gate:wait",
@@ -830,7 +840,7 @@ var REGISTRY = [
830
840
  scopes: ["user"],
831
841
  default: [],
832
842
  merge: "replace",
833
- 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.'
843
+ description: 'Event-bus glob rules that raise a desktop notification: [{pattern, category, title, message, subjectPrefix?, url?, owner?, surface?}]. 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. The optional surface field (a string) matches only events whose payload.origin.surface equals the given value, allowing a rule to fire only for gates opened by a specific surface (e.g. surface: "board" matches only board-originated gates). 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 board-specific rule can combine surface: "board" with a board URL to open the decision queue directly. A fresh key, not an ownership-latch port, so a default is fine here.'
834
844
  },
835
845
  {
836
846
  key: "rt.cron",
@@ -1031,6 +1041,14 @@ var REGISTRY = [
1031
1041
  merge: "replace",
1032
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.'
1033
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
+ },
1034
1052
  {
1035
1053
  key: "rt.integrations",
1036
1054
  type: "object",
@@ -2253,6 +2271,8 @@ export {
2253
2271
  resolveNameToIdentity,
2254
2272
  resolveForgeToken,
2255
2273
  repoNameForPath,
2274
+ reconcilerStatus,
2275
+ reconcilerClear,
2256
2276
  readStore,
2257
2277
  readProjectMRs,
2258
2278
  readMrsByBranch,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mattstack/rt-client",
3
- "version": "0.19.1",
3
+ "version": "0.21.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(
package/src/commands.ts CHANGED
@@ -95,12 +95,15 @@ 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;
101
103
  tabId?: string;
102
104
  runId?: string;
103
105
  worktree?: string;
106
+ surface?: string;
104
107
  presentation?: "form" | "wait";
105
108
  }
106
109
  export interface GateQuestion { id: string; label: string; multi: boolean; options: GateOption[] }
@@ -118,15 +121,23 @@ export interface GateRow {
118
121
  origin?: GateOrigin | null;
119
122
  status: GateStatus; answer: GateAnswer | null;
120
123
  openedAt: number; parkedAt: number | null; closedAt: number | null;
121
- closedReason: "abandoned" | "superseded" | "pruned" | null;
124
+ /** "resolved" is stamped only by the reconciler's auto-recovery close
125
+ (a blocked/gone pane came back); every other closer uses one of the
126
+ other three. */
127
+ closedReason: "abandoned" | "superseded" | "pruned" | "resolved" | null;
122
128
  /** Set only when `closedReason` is "superseded": the id of the gate that superseded this one. */
123
129
  supersededBy: string | null;
124
130
  agent: string | null; pane: string | null;
125
131
  nudge: { session: string } | null;
126
- delivery: { outcome: "delivered" | "dead-pane"; at: number } | null;
132
+ delivery: { outcome: "delivered" | "dead-pane" | "confirmed" | "stuck"; at: number } | null;
127
133
  released: boolean;
128
134
  owner: string | null;
129
135
  escalatedAt: number | null;
136
+ /** Set by answer-time execution handling: an answered gate whose executor
137
+ couldn't be resumed. Cleared (absent) once resumption succeeds. */
138
+ execution?: "unassigned";
139
+ /** Stamped by the reconciler sweep for open/parked rows only. */
140
+ executor?: ExecutorState;
130
141
  }
131
142
 
132
143
  export interface GateSubscription {
@@ -147,7 +158,7 @@ export interface HerdJobInfo { herd: string; name: string; worktree: string; bra
147
158
  /** `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. */
148
159
  export interface HerdStatusData {
149
160
  herd: HerdInfo;
150
- jobs: Array<HerdJobInfo & { openGate: string | null; paneStatus: string | null; lastGateStatus: GateStatus | null; lastGateDelivery: "delivered" | "dead-pane" | null }>;
161
+ jobs: Array<HerdJobInfo & { openGate: string | null; paneStatus: string | null; lastGateStatus: GateStatus | null; lastGateDelivery: "delivered" | "dead-pane" | "confirmed" | "stuck" | null }>;
151
162
  unread: number;
152
163
  lifecycleConnected: boolean;
153
164
  hiddenUp: boolean | null;
@@ -251,7 +262,8 @@ export interface InviteResult { paneId: string; delivered: "accepted" | "queued"
251
262
 
252
263
  /** Duplicated shape on purpose: mirrors lib/daemon/inject.ts's InjectResult. */
253
264
  export type PaneDelivery = "accepted" | "queued" | "refused";
254
- export interface PaneSendResult { paneId: string; delivered: PaneDelivery; reason?: string }
265
+ /** `continuation` is present only when the daemon scheduled a second line for after the target's current turn. */
266
+ export interface PaneSendResult { paneId: string; delivered: PaneDelivery; reason?: string; continuation?: { delivered: "deferred" } }
255
267
  /** `attendTab` is set only for a `bg:` ref: focus for a background pane IS
256
268
  the attend flow (a visible tab running a terminal attach), and this is
257
269
  that tab's id. */
@@ -305,11 +317,36 @@ export interface RunDetail { run: RunSummary; stages: RunStageRow[]; fields: Run
305
317
 
306
318
  export type AgentSurface = "herdr" | "headless";
307
319
 
320
+ /** The reconciler's per-agent snapshot: one row per agent it knows about, live or not. */
321
+ export interface ExecutorView {
322
+ agentId: string;
323
+ repo: string | null;
324
+ subject: string | null;
325
+ surface: AgentSurface;
326
+ sessionId: string;
327
+ paneRef: string | null;
328
+ state: ExecutorState;
329
+ since: number;
330
+ openGateIds: string[];
331
+ }
332
+
333
+ export interface ReconcilerStatus {
334
+ sweptAt: number;
335
+ herdrReachable: boolean;
336
+ executors: ExecutorView[];
337
+ }
338
+
308
339
  export interface AgentRecord {
309
340
  id: string; repo: string; cwd: string; provider: string;
310
341
  surface: AgentSurface; sessionId: string;
311
342
  model?: string; effort?: string; account?: string;
312
343
  label?: string; caller?: string; handle?: string;
344
+ /** The gate-protocol subject stamped as RT_GATE_SUBJECT at launch. Left
345
+ undefined when the caller passed none (RT_GATE_SUBJECT still falls
346
+ back to "agent:<id>" at launch time); an explicit value is persisted
347
+ so a resume re-stamps the same one, AND gates whether the gate-fork
348
+ PreToolUse hook gets injected at all. */
349
+ subject?: string;
313
350
  paneId?: string; tabId?: string; workspaceId?: string;
314
351
  extraArgs?: string; exitCode?: number; resultPath?: string;
315
352
  createdAt: number; lastResumedAt?: number; finishedAt?: number;
@@ -568,7 +605,7 @@ export interface Commands {
568
605
  "chat:dm-open": { payload: { from: string; to: string; sessionId?: string }; data: { room: string; created: boolean } };
569
606
 
570
607
  // ─── Agent handoff (rt agent) ────────────────────────────────────────────
571
- "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 };
608
+ "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 };
572
609
  "agent:resume": { payload: { id: string; prompt?: string; surface?: AgentSurface; workspace?: string; tab?: string }; data: AgentRecord };
573
610
  "agent:get": { payload: { id: string }; data: AgentRecord };
574
611
  "agent:list": { payload: { repo?: string }; data: { agents: AgentRecord[] } };
@@ -581,7 +618,7 @@ export interface Commands {
581
618
  payload: { cwd: string; account?: string; model?: string; effort?: string; prompt?: string; workspace?: string };
582
619
  data: { pane: ChatPane; ready: boolean };
583
620
  };
584
- "pane:send": { payload: { paneId: string; text: string; callerPane?: string }; data: PaneSendResult };
621
+ "pane:send": { payload: { paneId: string; text: string; callerPane?: string; continuation?: string }; data: PaneSendResult };
585
622
  /** `callerWorkspace` (HERDR_WORKSPACE_ID) is required only for a `bg:`
586
623
  ref, whose focus opens an attend tab in the caller's own workspace. */
587
624
  "pane:focus": { payload: { paneId: string; callerWorkspace?: string }; data: PaneFocusResult };
@@ -619,6 +656,12 @@ export interface Commands {
619
656
  "repos:locate": { payload: { newPath: string; repo?: string; dryRun?: boolean }; data: unknown };
620
657
  "freshness:reconcile": { payload: Record<string, never>; data: unknown };
621
658
 
659
+ // ─── Reconciler (executor state; lib/daemon/reconciler.ts) ───────────────
660
+ /** Read-only: the last sweep's snapshot. Never triggers a sweep itself. */
661
+ "reconciler:status": { payload: Record<string, never>; data: ReconcilerStatus };
662
+ /** Manual override: marks the agent cleared and closes its open/parked gates (reconciler.ts's clear()). */
663
+ "reconciler:clear": { payload: { agentId: string }; data: { cleared: true } };
664
+
622
665
  // ─── Gate facility (BOARD-20/21) ─────────────────────────────────────────
623
666
  "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 } };
624
667
  /**
@@ -769,6 +812,8 @@ export const COMMAND_NAMES: readonly CommandName[] = [
769
812
  "endpoint:status",
770
813
  "repos:locate",
771
814
  "freshness:reconcile",
815
+ "reconciler:status",
816
+ "reconciler:clear",
772
817
  "gate:open",
773
818
  "gate:answer",
774
819
  "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,
@@ -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?, 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.",
88
+ description: "Event-bus glob rules that raise a desktop notification: [{pattern, category, title, message, subjectPrefix?, url?, owner?, surface?}]. 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. The optional surface field (a string) matches only events whose payload.origin.surface equals the given value, allowing a rule to fire only for gates opened by a specific surface (e.g. surface: \"board\" matches only board-originated gates). 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 board-specific rule can combine surface: \"board\" with a board URL to open the decision queue directly. 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",
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
  /**