@mattstack/rt-client 0.19.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -218,7 +218,10 @@ export declare function herdAttend(a: Commands["herd:attend"]["payload"], o?: Rt
218
218
  export declare function herdWrapUp(a: Commands["herd:wrap-up"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["herd:wrap-up"]["data"]>>;
219
219
  /** Runs `herdr session stop`, one CLI call under the runner's own 15s budget. */
220
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. */
221
+ /** A cold ensure spawns `herdr server`, waits out bg-service's own 10s
222
+ readyTimeoutMs, then runs its parity probes sequentially -- the 15s
223
+ default budget every other rt-client call inherits can lose that race,
224
+ so this is the one wrapper that widens its own. */
222
225
  export declare function bgEnsure(a?: Commands["bg:ensure"]["payload"], o?: RtClientOptions): Promise<RtResponse<Commands["bg:ensure"]["data"]>>;
223
226
  /** Never ensures/spawns; a plain read of the current state. */
224
227
  export declare function bgStatus(o?: RtClientOptions): Promise<RtResponse<Commands["bg:status"]["data"]>>;
@@ -128,6 +128,7 @@ export interface GateAnswer {
128
128
  }>;
129
129
  by: string;
130
130
  answeredAt: number;
131
+ overridden?: boolean;
131
132
  }
132
133
  export interface GateRow {
133
134
  id: string;
@@ -143,6 +144,8 @@ export interface GateRow {
143
144
  parkedAt: number | null;
144
145
  closedAt: number | null;
145
146
  closedReason: "abandoned" | "superseded" | "pruned" | null;
147
+ /** Set only when `closedReason` is "superseded": the id of the gate that superseded this one. */
148
+ supersededBy: string | null;
146
149
  agent: string | null;
147
150
  pane: string | null;
148
151
  nudge: {
@@ -153,6 +156,8 @@ export interface GateRow {
153
156
  at: number;
154
157
  } | null;
155
158
  released: boolean;
159
+ owner: string | null;
160
+ escalatedAt: number | null;
156
161
  }
157
162
  export interface GateSubscription {
158
163
  id: string;
@@ -164,6 +169,8 @@ export interface GateSubscription {
164
169
  at: number;
165
170
  } | null;
166
171
  dead: boolean;
172
+ scope: "prefix" | "owner";
173
+ ownerRef: string | null;
167
174
  }
168
175
  export interface HerdInfo {
169
176
  id: string;
@@ -217,6 +224,11 @@ export interface HerdStatusData {
217
224
  dead: boolean;
218
225
  lastDelivery: GateSubscription["lastDelivery"];
219
226
  } | null;
227
+ /** 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. */
228
+ push: {
229
+ state: "reachable" | "unreachable";
230
+ lastDelivery: GateSubscription["lastDelivery"];
231
+ };
220
232
  }
221
233
  /**
222
234
  * Duplicated shape on purpose, same reasoning as EventsBusEvent above:
@@ -616,9 +628,12 @@ export interface WorktreeCreateData {
616
628
  }
617
629
  export interface WorktreeDisposeData {
618
630
  disposed: string[];
631
+ /** `detail` is set only for a refusal whose bare `reason` code can't name
632
+ what a human needs to act on it (the run id and stage for `running-run`). */
619
633
  refused: Array<{
620
634
  tree: string;
621
635
  reason: string;
636
+ detail?: string;
622
637
  }>;
623
638
  recoverable: Array<{
624
639
  tree: string;
@@ -644,6 +659,7 @@ export interface WorktreeAdoptData {
644
659
  refused: Array<{
645
660
  tree: string;
646
661
  reason: string;
662
+ detail?: string;
647
663
  }>;
648
664
  }
649
665
  /** Duplicated shape on purpose: mirrors lib/endpoint/store.ts's EndpointClaim. */
@@ -1368,12 +1384,21 @@ export interface Commands {
1368
1384
  * `conflict:true` and the WINNING row, so every consumer gets the winner
1369
1385
  * typed with no envelope hacks. `ok:false` is reserved for
1370
1386
  * not-found/closed/validation failures.
1387
+ *
1388
+ * Owner enforcement adds two structured rejections beyond the plain
1389
+ * `{ok:false, error:string}` shape (see `GateAnswerResult` in
1390
+ * `lib/daemon/handlers/gate.ts`): a herd-owned gate answered by anyone but
1391
+ * the owning shepherd's session, the answering pane, or an explicit human
1392
+ * `override` returns `{ok:false, error:"owned-by", owner}`; a closed gate
1393
+ * returns `{ok:false, error:"gate-closed", reason, supersededBy?}`.
1371
1394
  */
1372
1395
  "gate:answer": {
1373
1396
  payload: {
1374
1397
  id: string;
1375
1398
  answers: GateAnswer["answers"];
1376
1399
  by: string;
1400
+ session?: string;
1401
+ override?: boolean;
1377
1402
  };
1378
1403
  data: {
1379
1404
  row: GateRow;
@@ -1430,6 +1455,8 @@ export interface Commands {
1430
1455
  payload: {
1431
1456
  subjectPrefix: string;
1432
1457
  session: string;
1458
+ scope?: "owner";
1459
+ ownerRef?: string;
1433
1460
  };
1434
1461
  data: {
1435
1462
  id: string;
@@ -1505,7 +1532,8 @@ export interface Commands {
1505
1532
  };
1506
1533
  data: {
1507
1534
  job: string;
1508
- status: "closed";
1535
+ status: "closed"; /** Advisory: a resumable run can still write into this job's worktree after close, so this warns rather than blocking. */
1536
+ warning?: string;
1509
1537
  };
1510
1538
  };
1511
1539
  /** `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.js CHANGED
@@ -281,7 +281,11 @@ function gateOpen(a, o = {}) {
281
281
  return rtCommand("gate:open", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
282
282
  }
283
283
  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 });
284
+ const payload = { id: a.id, answers: a.answers, by: a.by };
285
+ for (const k of ["session", "override"])
286
+ if (a[k] !== undefined)
287
+ payload[k] = a[k];
288
+ return rtCommand("gate:answer", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
285
289
  }
286
290
  function gateWait(a, o = {}) {
287
291
  const payload = { id: a.id };
@@ -303,7 +307,11 @@ function gateClose(a, o = {}) {
303
307
  return rtCommand("gate:close", { id: a.id, reason: a.reason }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
304
308
  }
305
309
  function gateSubscribe(a, o = {}) {
306
- return rtCommand("gate:subscribe", { subjectPrefix: a.subjectPrefix, session: a.session }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
310
+ const payload = { subjectPrefix: a.subjectPrefix, session: a.session };
311
+ for (const k of ["scope", "ownerRef"])
312
+ if (a[k] !== undefined)
313
+ payload[k] = a[k];
314
+ return rtCommand("gate:subscribe", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
307
315
  }
308
316
  function gateUnsubscribe(a, o = {}) {
309
317
  return rtCommand("gate:unsubscribe", { id: a.id }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
@@ -383,7 +391,7 @@ function bgEnsure(a = {}, o = {}) {
383
391
  const payload = {};
384
392
  if (a.claim !== undefined)
385
393
  payload.claim = a.claim;
386
- return rtCommand("bg:ensure", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 15000 });
394
+ return rtCommand("bg:ensure", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30000 });
387
395
  }
388
396
  function bgStatus(o = {}) {
389
397
  return rtCommand("bg:status", {}, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
@@ -705,6 +713,9 @@ function formatPaneRef(paneId, server) {
705
713
  if (server === "visible") {
706
714
  return paneId;
707
715
  }
716
+ if (paneId.startsWith(BG_PREFIX)) {
717
+ return paneId;
718
+ }
708
719
  return BG_PREFIX + paneId;
709
720
  }
710
721
  // src/settings/resolve.ts
@@ -777,7 +788,7 @@ var REGISTRY = [
777
788
  merge: "deep",
778
789
  repoScoped: true,
779
790
  migrated: true,
780
- description: "Per-repo worktree pool config (onDeck size, ready steps, name pool); root/branchFormat/ready computed-or-empty in the reader."
791
+ description: "Per-repo worktree pool config (onDeck size, ready steps, name pool, staleClaimDays); root/branchFormat/ready computed-or-empty in the reader."
781
792
  },
782
793
  {
783
794
  key: "rt.worktreeReadyApproval",
@@ -819,7 +830,7 @@ var REGISTRY = [
819
830
  scopes: ["user"],
820
831
  default: [],
821
832
  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."
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.'
823
834
  },
824
835
  {
825
836
  key: "rt.cron",
@@ -1138,7 +1149,7 @@ var REGISTRY = [
1138
1149
  type: "array",
1139
1150
  scopes: ["team"],
1140
1151
  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)."
1152
+ 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
1153
  },
1143
1154
  {
1144
1155
  key: "board.staleAfterDays",
@@ -1381,6 +1392,14 @@ var REGISTRY = [
1381
1392
  scopes: ["user", "machine"],
1382
1393
  merge: "replace",
1383
1394
  description: "Opaque extra claude arguments appended to every rt agent launch (escape hatch)."
1395
+ },
1396
+ {
1397
+ key: "rt.gates.escalationTtlMinutes",
1398
+ type: "number",
1399
+ scopes: ["user"],
1400
+ default: 10,
1401
+ merge: "replace",
1402
+ 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
1403
  }
1385
1404
  ];
1386
1405
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mattstack/rt-client",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/client.ts CHANGED
@@ -461,7 +461,9 @@ export function gateAnswer(
461
461
  a: Commands["gate:answer"]["payload"],
462
462
  o: RtClientOptions = {},
463
463
  ): 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 });
464
+ const payload: Record<string, unknown> = { id: a.id, answers: a.answers, by: a.by };
465
+ for (const k of ["session", "override"] as const) if (a[k] !== undefined) payload[k] = a[k];
466
+ return rtCommand<Commands["gate:answer"]["data"]>("gate:answer", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
465
467
  }
466
468
 
467
469
  /** Daemon clamps its own wait to 240s (gates-store.ts); the client abort
@@ -503,7 +505,9 @@ export function gateSubscribe(
503
505
  a: Commands["gate:subscribe"]["payload"],
504
506
  o: RtClientOptions = {},
505
507
  ): 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 });
508
+ const payload: Record<string, unknown> = { subjectPrefix: a.subjectPrefix, session: a.session };
509
+ for (const k of ["scope", "ownerRef"] as const) if (a[k] !== undefined) payload[k] = a[k];
510
+ return rtCommand<Commands["gate:subscribe"]["data"]>("gate:subscribe", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
507
511
  }
508
512
 
509
513
  export function gateUnsubscribe(
@@ -645,14 +649,17 @@ export function herdStopHidden(
645
649
 
646
650
  // ─── Background server (daemon-owned background herdr session) ────────────
647
651
 
648
- /** May spawn `herdr server` and wait for it to bind; budget matches bg-service's own 10s readyTimeoutMs plus margin. */
652
+ /** A cold ensure spawns `herdr server`, waits out bg-service's own 10s
653
+ readyTimeoutMs, then runs its parity probes sequentially -- the 15s
654
+ default budget every other rt-client call inherits can lose that race,
655
+ so this is the one wrapper that widens its own. */
649
656
  export function bgEnsure(
650
657
  a: Commands["bg:ensure"]["payload"] = {},
651
658
  o: RtClientOptions = {},
652
659
  ): Promise<RtResponse<Commands["bg:ensure"]["data"]>> {
653
660
  const payload: Record<string, unknown> = {};
654
661
  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 });
662
+ return rtCommand<Commands["bg:ensure"]["data"]>("bg:ensure", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30_000 });
656
663
  }
657
664
 
658
665
  /** Never ensures/spawns; a plain read of the current state. */
package/src/commands.ts CHANGED
@@ -110,7 +110,7 @@ export function gateOptionValue(o: GateOption): string {
110
110
  export function gateOptionLabel(o: GateOption): string {
111
111
  return typeof o === "string" ? o : (o.label || o.value);
112
112
  }
113
- export interface GateAnswer { answers: Record<string, string | string[] | { value: string | string[]; note?: string }>; by: string; answeredAt: number }
113
+ export interface GateAnswer { answers: Record<string, string | string[] | { value: string | string[]; note?: string }>; by: string; answeredAt: number; overridden?: boolean }
114
114
  export interface GateRow {
115
115
  id: string; subject: string; kind: string;
116
116
  questions: GateQuestion[]; meta: Record<string, unknown> | null;
@@ -119,10 +119,14 @@ export interface GateRow {
119
119
  status: GateStatus; answer: GateAnswer | null;
120
120
  openedAt: number; parkedAt: number | null; closedAt: number | null;
121
121
  closedReason: "abandoned" | "superseded" | "pruned" | null;
122
+ /** Set only when `closedReason` is "superseded": the id of the gate that superseded this one. */
123
+ supersededBy: string | null;
122
124
  agent: string | null; pane: string | null;
123
125
  nudge: { session: string } | null;
124
126
  delivery: { outcome: "delivered" | "dead-pane"; at: number } | null;
125
127
  released: boolean;
128
+ owner: string | null;
129
+ escalatedAt: number | null;
126
130
  }
127
131
 
128
132
  export interface GateSubscription {
@@ -132,6 +136,8 @@ export interface GateSubscription {
132
136
  createdAt: number;
133
137
  lastDelivery: { outcome: "delivered" | "failed"; at: number } | null;
134
138
  dead: boolean;
139
+ scope: "prefix" | "owner";
140
+ ownerRef: string | null;
135
141
  }
136
142
 
137
143
  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 }
@@ -147,6 +153,8 @@ export interface HerdStatusData {
147
153
  hiddenUp: boolean | null;
148
154
  /** The shepherd session's own `herd:<id>/` subscription row, or null when none is live. */
149
155
  subscription: { id: string; dead: boolean; lastDelivery: GateSubscription["lastDelivery"] } | null;
156
+ /** 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. */
157
+ push: { state: "reachable" | "unreachable"; lastDelivery: GateSubscription["lastDelivery"] };
150
158
  }
151
159
 
152
160
  /**
@@ -405,7 +413,9 @@ export interface WorktreeProvisionData {
405
413
  export interface WorktreeCreateData { tree: string; path: string }
406
414
  export interface WorktreeDisposeData {
407
415
  disposed: string[];
408
- refused: Array<{ tree: string; reason: string }>;
416
+ /** `detail` is set only for a refusal whose bare `reason` code can't name
417
+ what a human needs to act on it (the run id and stage for `running-run`). */
418
+ refused: Array<{ tree: string; reason: string; detail?: string }>;
409
419
  recoverable: Array<{ tree: string; path: string; until: string }>;
410
420
  }
411
421
  export interface WorktreeRestoreData {
@@ -414,7 +424,7 @@ export interface WorktreeRestoreData {
414
424
  export interface WorktreeFreshenData { ran: string[] }
415
425
  export interface WorktreeAdoptData {
416
426
  main: string; claimed: string[]; unmanaged: string[]; disposed: string[];
417
- refused: Array<{ tree: string; reason: string }>;
427
+ refused: Array<{ tree: string; reason: string; detail?: string }>;
418
428
  }
419
429
 
420
430
  /** Duplicated shape on purpose: mirrors lib/endpoint/store.ts's EndpointClaim. */
@@ -616,8 +626,15 @@ export interface Commands {
616
626
  * `conflict:true` and the WINNING row, so every consumer gets the winner
617
627
  * typed with no envelope hacks. `ok:false` is reserved for
618
628
  * not-found/closed/validation failures.
629
+ *
630
+ * Owner enforcement adds two structured rejections beyond the plain
631
+ * `{ok:false, error:string}` shape (see `GateAnswerResult` in
632
+ * `lib/daemon/handlers/gate.ts`): a herd-owned gate answered by anyone but
633
+ * the owning shepherd's session, the answering pane, or an explicit human
634
+ * `override` returns `{ok:false, error:"owned-by", owner}`; a closed gate
635
+ * returns `{ok:false, error:"gate-closed", reason, supersededBy?}`.
619
636
  */
620
- "gate:answer": { payload: { id: string; answers: GateAnswer["answers"]; by: string }; data: { row: GateRow; conflict?: true } };
637
+ "gate:answer": { payload: { id: string; answers: GateAnswer["answers"]; by: string; session?: string; override?: boolean }; data: { row: GateRow; conflict?: true } };
621
638
  /** `ok:false "not-found"` on an unknown id is terminal; the CLI loop must not re-enter on it.
622
639
  * `timeout` carries no row (nothing settled); `answered`/`closed` always carry the settled row. */
623
640
  "gate:wait": { payload: { id: string; waitMs?: number }; data: { status: "timeout" } | { status: "answered" | "closed"; row: GateRow } };
@@ -626,7 +643,7 @@ export interface Commands {
626
643
  "gate:list": { payload: { open?: boolean; subjectPrefix?: string; kind?: string; limit?: number; cursor?: number }; data: { gates: GateRow[]; cursor: number } };
627
644
  "gate:park": { payload: { id: string }; data: { ok: true } };
628
645
  "gate:close": { payload: { id: string; reason: "abandoned" | "superseded" | "pruned" }; data: { ok: true } };
629
- "gate:subscribe": { payload: { subjectPrefix: string; session: string }; data: { id: string } };
646
+ "gate:subscribe": { payload: { subjectPrefix: string; session: string; scope?: "owner"; ownerRef?: string }; data: { id: string } };
630
647
  "gate:unsubscribe": { payload: { id: string }; data: { removed: boolean } };
631
648
  /** The shepherd's gap-recovery liveness check and the observability window
632
649
  * onto delivery outcomes (dead marks included). */
@@ -638,7 +655,7 @@ export interface Commands {
638
655
  "herd:status": { payload: { herd: string }; data: HerdStatusData };
639
656
  /** Active herds only unless `all`, so a shepherd's "which herd am I on" question has one answer. */
640
657
  "herd:list": { payload: { all?: boolean }; data: { herds: HerdListRow[] } };
641
- "herd:close": { payload: { herd: string; job: string }; data: { job: string; status: "closed" } };
658
+ "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
659
  /** `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
660
  "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
661
  "herd:gates": { payload: { herd: string }; data: { gates: GateRow[] } };
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",
@@ -427,7 +427,7 @@ export const REGISTRY: readonly SettingDef[] = [
427
427
  type: "array",
428
428
  scopes: ["team"],
429
429
  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).",
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 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
431
  },
432
432
 
433
433
  // --- board (user) ----------------------------------------------------------
@@ -687,4 +687,14 @@ export const REGISTRY: readonly SettingDef[] = [
687
687
  merge: "replace",
688
688
  description: "Opaque extra claude arguments appended to every rt agent launch (escape hatch).",
689
689
  },
690
+
691
+ // --- gates (escalation) ----------------------------------------------------
692
+ {
693
+ key: "rt.gates.escalationTtlMinutes",
694
+ type: "number",
695
+ scopes: ["user"],
696
+ default: 10,
697
+ merge: "replace",
698
+ 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.",
699
+ },
690
700
  ];