@mattstack/rt-client 0.25.0 → 0.27.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.
@@ -108,6 +108,7 @@ export type GateOption = string | {
108
108
  value: string;
109
109
  label: string;
110
110
  recommended?: boolean;
111
+ description?: string;
111
112
  };
112
113
  export interface GateOrigin {
113
114
  paneId?: string;
@@ -117,11 +118,14 @@ export interface GateOrigin {
117
118
  surface?: string;
118
119
  presentation?: "form" | "wait";
119
120
  }
121
+ /** `context` is per-question material (what this one choice turns on);
122
+ the gate-level `context` on the open/ask payload is the whole ask's. */
120
123
  export interface GateQuestion {
121
124
  id: string;
122
125
  label: string;
123
126
  multi: boolean;
124
127
  options: GateOption[];
128
+ context?: string;
125
129
  }
126
130
  /** Implementations live in gate-options.ts (the browser-safe ./gate
127
131
  subpath); re-exported here so existing commands.ts/index.ts consumers
@@ -171,14 +175,13 @@ export interface GateRow {
171
175
  at: number;
172
176
  } | null;
173
177
  released: boolean;
174
- /** Set once the nudged pane has provably read the answer: either it
175
- self-answered (stamped in the same transaction as the answer) or a
176
- later `markConsumed` call recorded that it acted on a push. `null`
177
- until then, so a sweep can tell an answered-but-unread row from a
178
- settled one. Currently only stamped for herd-subject gates (the
179
- self-answer path and `rt herd answer`'s nudged-session read); a
180
- non-herd gate with a nudge stays `null` even after its pane
181
- reconciles. */
178
+ /** Set once the nudged pane has provably read the answer: it self-answered
179
+ or lost the CAS to one (both stamped in the answer's own transaction),
180
+ or a later `markConsumed` call recorded that it acted on a push
181
+ (`rt herd answer` and `rt gate wait`, each on the nudged session's own
182
+ read). `null` until then, so a sweep can tell an answered-but-unread row
183
+ from a settled one. Every nudge-bearing subject has a stamping read;
184
+ a gate with no nudge never carries one, having no pane to read it. */
182
185
  consumedAt: number | null;
183
186
  owner: string | null;
184
187
  escalatedAt: number | null;
@@ -228,19 +231,20 @@ export interface HerdJobInfo {
228
231
  agentSession: string | null;
229
232
  agentId: string | null;
230
233
  handle: string;
231
- status: "spawning" | "active" | "at-gate" | "at-milestone" | "done" | "closed" | "crashed";
234
+ status: "spawning" | "active" | "at-gate" | "at-milestone" | "done" | "closed" | "crashed" | "stuck-at-modal";
232
235
  disposable: boolean;
233
236
  lastGate: string | null;
234
237
  lastReport: number | null;
235
238
  createdAt: number;
236
239
  updatedAt: number;
237
240
  }
238
- /** `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. `lastGateConsumed` is `null` when there is nothing to consume (no last gate, not answered, or not nudged), and otherwise reports whether the nudged pane has read its answer. */
241
+ /** `lastGateStatus`/`lastGateDelivery` come from the job's `lastGate` row: a TERMINAL gate (answered or closed) whose delivery is `dead-pane` is the "worker not woken" case the shepherd must act on. `lastGateConsumed` is `null` when there is nothing to consume (no last gate, not answered, or not nudged), and otherwise reports whether the nudged pane has read its answer. */
239
242
  export interface HerdStatusData {
240
243
  herd: HerdInfo;
241
244
  jobs: Array<HerdJobInfo & {
242
245
  openGate: string | null;
243
- paneStatus: string | null;
246
+ paneStatus: string | null; /** The worker session behind this job's pane is gone while the job still reads in-flight: herdr lists the pane but no claude is on it. Null when herdr does not list the pane at all (it closed, or herdr is unreachable), which proves nothing either way. */
247
+ sessionDead: boolean | null;
244
248
  lastGateStatus: GateStatus | null;
245
249
  lastGateDelivery: "delivered" | "dead-pane" | "confirmed" | "stuck" | null;
246
250
  lastGateConsumed: boolean | null;
@@ -507,6 +511,7 @@ export interface AgentRecord {
507
511
  extraArgs?: string;
508
512
  exitCode?: number;
509
513
  resultPath?: string;
514
+ yolo?: boolean;
510
515
  createdAt: number;
511
516
  lastResumedAt?: number;
512
517
  finishedAt?: number;
@@ -1141,6 +1146,7 @@ export interface Commands {
1141
1146
  cwd: string;
1142
1147
  prompt?: string;
1143
1148
  surface?: AgentSurface;
1149
+ provider?: string;
1144
1150
  model?: string;
1145
1151
  effort?: string;
1146
1152
  account?: string;
@@ -1154,6 +1160,7 @@ export interface Commands {
1154
1160
  handle?: string;
1155
1161
  bg?: boolean;
1156
1162
  subject?: string;
1163
+ yolo?: boolean;
1157
1164
  };
1158
1165
  data: AgentRecord;
1159
1166
  };
@@ -1503,11 +1510,16 @@ export interface Commands {
1503
1510
  worktree?: string;
1504
1511
  };
1505
1512
  };
1513
+ /** `contextOmitted` appears only when the gate context plus every
1514
+ question's `context` exceeded their shared 8192-byte budget: the
1515
+ gate still opened, but question contexts were dropped, and the gate
1516
+ context too when it was over the budget on its own. */
1506
1517
  data: {
1507
1518
  id: string;
1508
1519
  presentation: "form" | "wait";
1509
1520
  subject: string;
1510
1521
  supersededId: string | null;
1522
+ contextOmitted?: true;
1511
1523
  };
1512
1524
  };
1513
1525
  /**
@@ -1538,10 +1550,14 @@ export interface Commands {
1538
1550
  };
1539
1551
  /** `ok:false "not-found"` on an unknown id is terminal; the CLI loop must not re-enter on it.
1540
1552
  * `timeout` carries no row (nothing settled); `answered`/`closed` always carry the settled row. */
1553
+ /** `sessionId` is the caller's own session: an answered result returned to
1554
+ * the gate's nudged session is a recorded read and stamps `consumedAt`.
1555
+ * Omitting it reads without consuming. */
1541
1556
  "gate:wait": {
1542
1557
  payload: {
1543
1558
  id: string;
1544
1559
  waitMs?: number;
1560
+ sessionId?: string;
1545
1561
  };
1546
1562
  data: {
1547
1563
  status: "timeout";
@@ -1689,7 +1705,8 @@ export interface Commands {
1689
1705
  wasOnDeck: boolean | null;
1690
1706
  agentId: string;
1691
1707
  sessionId: string;
1692
- handle: string;
1708
+ handle: string; /** What the folder-trust check established: no modal was up, one was accepted and verified gone, one is still up (the job reads `stuck-at-modal`), or herdr could not be read. */
1709
+ trust: "none" | "accepted" | "stuck" | "unchecked";
1693
1710
  };
1694
1711
  };
1695
1712
  "herd:gates": {
@@ -5,6 +5,9 @@ import type { GateOption, GateQuestion } from "./commands.ts";
5
5
  export interface GateOptionObject {
6
6
  value: string;
7
7
  label: string;
8
+ /** Per-option explanation, carried verbatim: never capitalized, never
9
+ suffixed, never filled in from value/label. */
10
+ description?: string;
8
11
  }
9
12
  export declare function gateOptionValue(o: GateOption): string;
10
13
  export declare function gateOptionLabel(o: GateOption): string;
@@ -17,8 +20,13 @@ export declare function gateOptionLabel(o: GateOption): string;
17
20
  word-like (see isWordLikeLabel), and an object form's `recommended:
18
21
  true` lifts into a " (Recommended)" label suffix -- guarded against
19
22
  double-appending -- since `recommended` itself does not survive into
20
- the returned object; the suffix IS its wire representation. Every
21
- returned entry is a full {value,label} pair -- callers may trust the
22
- return type without re-checking it. Pure and order-preserving. */
23
+ the returned object; the suffix IS its wire representation. An object
24
+ form's non-empty string `description` rides along untouched (no
25
+ capitalization, no suffix); any other description shape is dropped.
26
+ Every returned entry is a full {value,label} pair -- callers may trust
27
+ the return type without re-checking it. Pure and order-preserving. */
23
28
  export declare function normalizeGateOptions(options: GateOption[]): GateOptionObject[];
29
+ /** Options normalize per normalizeGateOptions; a question's `context` is
30
+ kept verbatim when it is a non-blank string and dropped otherwise, so a
31
+ stored row never carries an empty or non-string context key. */
24
32
  export declare function normalizeGateQuestions(questions: GateQuestion[]): GateQuestion[];
package/dist/gate.js CHANGED
@@ -33,11 +33,18 @@ function normalizeGateOptions(options) {
33
33
  label = capitalize(label);
34
34
  if (recommended && label && !HAS_RECOMMENDED_SUFFIX.test(label))
35
35
  label += RECOMMENDED_SUFFIX;
36
- return { value, label };
36
+ const d = o !== null && typeof o === "object" ? o.description : undefined;
37
+ return typeof d === "string" && d ? { value, label, description: d } : { value, label };
37
38
  });
38
39
  }
39
40
  function normalizeGateQuestions(questions) {
40
- return questions.map((q) => ({ ...q, options: normalizeGateOptions(q.options) }));
41
+ return questions.map((q) => {
42
+ const { context, ...rest } = q;
43
+ const out = { ...rest, options: normalizeGateOptions(q.options) };
44
+ if (typeof context === "string" && context.trim())
45
+ out.context = context;
46
+ return out;
47
+ });
41
48
  }
42
49
 
43
50
  // src/gate-answers.ts
package/dist/index.js CHANGED
@@ -243,7 +243,7 @@ function eventsList(payload, o = {}) {
243
243
  }
244
244
  function agentStart(a, o = {}) {
245
245
  const payload = { repo: a.repo, cwd: a.cwd };
246
- for (const k of ["prompt", "surface", "model", "effort", "account", "label", "caller", "workspace", "tab", "extraArgs", "env", "herdrSocket", "handle", "bg", "subject"]) {
246
+ for (const k of ["prompt", "surface", "provider", "model", "effort", "account", "label", "caller", "workspace", "tab", "extraArgs", "env", "herdrSocket", "handle", "bg", "subject", "yolo"]) {
247
247
  if (a[k] !== undefined)
248
248
  payload[k] = a[k];
249
249
  }
@@ -344,6 +344,8 @@ function gateWait(a, o = {}) {
344
344
  const payload = { id: a.id };
345
345
  if (a.waitMs !== undefined)
346
346
  payload.waitMs = a.waitMs;
347
+ if (a.sessionId !== undefined)
348
+ payload.sessionId = a.sessionId;
347
349
  return rtCommand("gate:wait", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 250000 });
348
350
  }
349
351
  function gateList(a, o = {}) {
@@ -491,11 +493,18 @@ function normalizeGateOptions(options) {
491
493
  label = capitalize(label);
492
494
  if (recommended && label && !HAS_RECOMMENDED_SUFFIX.test(label))
493
495
  label += RECOMMENDED_SUFFIX;
494
- return { value, label };
496
+ const d = o !== null && typeof o === "object" ? o.description : undefined;
497
+ return typeof d === "string" && d ? { value, label, description: d } : { value, label };
495
498
  });
496
499
  }
497
500
  function normalizeGateQuestions(questions) {
498
- return questions.map((q) => ({ ...q, options: normalizeGateOptions(q.options) }));
501
+ return questions.map((q) => {
502
+ const { context, ...rest } = q;
503
+ const out = { ...rest, options: normalizeGateOptions(q.options) };
504
+ if (typeof context === "string" && context.trim())
505
+ out.context = context;
506
+ return out;
507
+ });
499
508
  }
500
509
 
501
510
  // src/commands.ts
@@ -1519,32 +1528,75 @@ var REGISTRY = [
1519
1528
  description: "Destination (topic/URL/token) the configured chat.push.provider sends to."
1520
1529
  },
1521
1530
  {
1522
- key: "agent.model",
1531
+ key: "agent.provider",
1532
+ type: "string",
1533
+ scopes: ["user", "machine"],
1534
+ default: "claude",
1535
+ merge: "replace",
1536
+ description: 'Which provider rt agent start uses when --provider is not given: "claude" or "codex".'
1537
+ },
1538
+ {
1539
+ key: "agent.claude.model",
1540
+ type: "string",
1541
+ scopes: ["user", "machine"],
1542
+ merge: "replace",
1543
+ description: "Default --model for claude rt agent launches; unset omits the flag."
1544
+ },
1545
+ {
1546
+ key: "agent.claude.effort",
1547
+ type: "string",
1548
+ scopes: ["user", "machine"],
1549
+ merge: "replace",
1550
+ description: "Default --effort for claude rt agent launches; unset omits the flag."
1551
+ },
1552
+ {
1553
+ key: "agent.claude.account",
1554
+ type: "string",
1555
+ scopes: ["user", "machine"],
1556
+ merge: "replace",
1557
+ description: "cswap account email claude rt agent launches under; unset uses the default claude profile."
1558
+ },
1559
+ {
1560
+ key: "agent.claude.extraArgs",
1523
1561
  type: "string",
1524
1562
  scopes: ["user", "machine"],
1525
1563
  merge: "replace",
1526
- description: "Default --model for rt agent launches; unset omits the flag."
1564
+ description: "Opaque extra claude arguments appended to every claude rt agent launch (escape hatch)."
1527
1565
  },
1528
1566
  {
1529
- key: "agent.effort",
1567
+ key: "agent.claude.yolo",
1568
+ type: "boolean",
1569
+ scopes: ["user", "machine"],
1570
+ merge: "replace",
1571
+ description: "Default --yolo (--dangerously-skip-permissions) for claude rt agent launches; unset behaves as false."
1572
+ },
1573
+ {
1574
+ key: "agent.codex.model",
1530
1575
  type: "string",
1531
1576
  scopes: ["user", "machine"],
1532
1577
  merge: "replace",
1533
- description: "Default --effort for rt agent launches; unset omits the flag."
1578
+ description: "Default -m/--model for codex rt agent launches; unset omits the flag."
1534
1579
  },
1535
1580
  {
1536
- key: "agent.account",
1581
+ key: "agent.codex.effort",
1537
1582
  type: "string",
1538
1583
  scopes: ["user", "machine"],
1539
1584
  merge: "replace",
1540
- description: "cswap account email rt agent launches under; unset uses the default claude profile."
1585
+ description: "Default reasoning effort for codex rt agent launches, passed as -c model_reasoning_effort=<value>; unset omits the override."
1541
1586
  },
1542
1587
  {
1543
- key: "agent.extraArgs",
1588
+ key: "agent.codex.extraArgs",
1544
1589
  type: "string",
1545
1590
  scopes: ["user", "machine"],
1546
1591
  merge: "replace",
1547
- description: "Opaque extra claude arguments appended to every rt agent launch (escape hatch)."
1592
+ description: "Opaque extra codex arguments appended to every codex rt agent launch (escape hatch)."
1593
+ },
1594
+ {
1595
+ key: "agent.codex.yolo",
1596
+ type: "boolean",
1597
+ scopes: ["user", "machine"],
1598
+ merge: "replace",
1599
+ description: "Default --yolo (--dangerously-bypass-approvals-and-sandbox) for codex rt agent launches; unset behaves as false. codex has no per-agent account setting (see spec's Non-goals)."
1548
1600
  },
1549
1601
  {
1550
1602
  key: "rt.gates.escalationTtlMinutes",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mattstack/rt-client",
3
- "version": "0.25.0",
3
+ "version": "0.27.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", "subject"] as const) {
359
+ for (const k of ["prompt", "surface", "provider", "model", "effort", "account", "label", "caller", "workspace", "tab", "extraArgs", "env", "herdrSocket", "handle", "bg", "subject", "yolo"] 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 });
@@ -499,6 +499,7 @@ export function gateWait(
499
499
  ): Promise<RtResponse<Commands["gate:wait"]["data"]>> {
500
500
  const payload: Record<string, unknown> = { id: a.id };
501
501
  if (a.waitMs !== undefined) payload.waitMs = a.waitMs;
502
+ if (a.sessionId !== undefined) payload.sessionId = a.sessionId;
502
503
  return rtCommand<Commands["gate:wait"]["data"]>("gate:wait", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 250_000 });
503
504
  }
504
505
 
package/src/commands.ts CHANGED
@@ -97,7 +97,7 @@ export const GATE_BY_PANE = "pane";
97
97
  export type GateStatus = "open" | "answered" | "parked" | "closed";
98
98
  /** Reconciler's view of an agent's liveness; also the value `GateRow.executor` is stamped with. */
99
99
  export type ExecutorState = "live" | "blocked" | "hidden" | "gone" | "cleared" | "unknown";
100
- export type GateOption = string | { value: string; label: string; recommended?: boolean };
100
+ export type GateOption = string | { value: string; label: string; recommended?: boolean; description?: string };
101
101
  export interface GateOrigin {
102
102
  paneId?: string;
103
103
  tabId?: string;
@@ -106,7 +106,9 @@ export interface GateOrigin {
106
106
  surface?: string;
107
107
  presentation?: "form" | "wait";
108
108
  }
109
- export interface GateQuestion { id: string; label: string; multi: boolean; options: GateOption[] }
109
+ /** `context` is per-question material (what this one choice turns on);
110
+ the gate-level `context` on the open/ask payload is the whole ask's. */
111
+ export interface GateQuestion { id: string; label: string; multi: boolean; options: GateOption[]; context?: string }
110
112
  /** Implementations live in gate-options.ts (the browser-safe ./gate
111
113
  subpath); re-exported here so existing commands.ts/index.ts consumers
112
114
  are unaffected. */
@@ -134,14 +136,13 @@ export interface GateRow {
134
136
  nudge: { session: string } | null;
135
137
  delivery: { outcome: "delivered" | "dead-pane" | "confirmed" | "stuck"; at: number } | null;
136
138
  released: boolean;
137
- /** Set once the nudged pane has provably read the answer: either it
138
- self-answered (stamped in the same transaction as the answer) or a
139
- later `markConsumed` call recorded that it acted on a push. `null`
140
- until then, so a sweep can tell an answered-but-unread row from a
141
- settled one. Currently only stamped for herd-subject gates (the
142
- self-answer path and `rt herd answer`'s nudged-session read); a
143
- non-herd gate with a nudge stays `null` even after its pane
144
- reconciles. */
139
+ /** Set once the nudged pane has provably read the answer: it self-answered
140
+ or lost the CAS to one (both stamped in the answer's own transaction),
141
+ or a later `markConsumed` call recorded that it acted on a push
142
+ (`rt herd answer` and `rt gate wait`, each on the nudged session's own
143
+ read). `null` until then, so a sweep can tell an answered-but-unread row
144
+ from a settled one. Every nudge-bearing subject has a stamping read;
145
+ a gate with no nudge never carries one, having no pane to read it. */
145
146
  consumedAt: number | null;
146
147
  owner: string | null;
147
148
  escalatedAt: number | null;
@@ -166,11 +167,11 @@ export interface GateSubscription {
166
167
  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 }
167
168
  /** A herd row as `herd:list` reports it: the registry row plus how many jobs hang off it. */
168
169
  export interface HerdListRow extends HerdInfo { jobs: number }
169
- export interface HerdJobInfo { herd: string; name: string; worktree: string; branch: string | null; tree: string | null; pane: string | null; agentSession: string | null; agentId: string | null; handle: string; status: "spawning" | "active" | "at-gate" | "at-milestone" | "done" | "closed" | "crashed"; disposable: boolean; lastGate: string | null; lastReport: number | null; createdAt: number; updatedAt: number }
170
- /** `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. `lastGateConsumed` is `null` when there is nothing to consume (no last gate, not answered, or not nudged), and otherwise reports whether the nudged pane has read its answer. */
170
+ export interface HerdJobInfo { herd: string; name: string; worktree: string; branch: string | null; tree: string | null; pane: string | null; agentSession: string | null; agentId: string | null; handle: string; status: "spawning" | "active" | "at-gate" | "at-milestone" | "done" | "closed" | "crashed" | "stuck-at-modal"; disposable: boolean; lastGate: string | null; lastReport: number | null; createdAt: number; updatedAt: number }
171
+ /** `lastGateStatus`/`lastGateDelivery` come from the job's `lastGate` row: a TERMINAL gate (answered or closed) whose delivery is `dead-pane` is the "worker not woken" case the shepherd must act on. `lastGateConsumed` is `null` when there is nothing to consume (no last gate, not answered, or not nudged), and otherwise reports whether the nudged pane has read its answer. */
171
172
  export interface HerdStatusData {
172
173
  herd: HerdInfo;
173
- jobs: Array<HerdJobInfo & { openGate: string | null; paneStatus: string | null; lastGateStatus: GateStatus | null; lastGateDelivery: "delivered" | "dead-pane" | "confirmed" | "stuck" | null; lastGateConsumed: boolean | null }>;
174
+ jobs: Array<HerdJobInfo & { openGate: string | null; paneStatus: string | null; /** The worker session behind this job's pane is gone while the job still reads in-flight: herdr lists the pane but no claude is on it. Null when herdr does not list the pane at all (it closed, or herdr is unreachable), which proves nothing either way. */ sessionDead: boolean | null; lastGateStatus: GateStatus | null; lastGateDelivery: "delivered" | "dead-pane" | "confirmed" | "stuck" | null; lastGateConsumed: boolean | null }>;
174
175
  unread: number;
175
176
  lifecycleConnected: boolean;
176
177
  hiddenUp: boolean | null;
@@ -360,7 +361,7 @@ export interface AgentRecord {
360
361
  PreToolUse hook gets injected at all. */
361
362
  subject?: string;
362
363
  paneId?: string; tabId?: string; workspaceId?: string;
363
- extraArgs?: string; exitCode?: number; resultPath?: string;
364
+ extraArgs?: string; exitCode?: number; resultPath?: string; yolo?: boolean;
364
365
  createdAt: number; lastResumedAt?: number; finishedAt?: number;
365
366
  }
366
367
 
@@ -617,7 +618,7 @@ export interface Commands {
617
618
  "chat:dm-open": { payload: { from: string; to: string; sessionId?: string }; data: { room: string; created: boolean } };
618
619
 
619
620
  // ─── Agent handoff (rt agent) ────────────────────────────────────────────
620
- "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 };
621
+ "agent:start": { payload: { repo: string; cwd: string; prompt?: string; surface?: AgentSurface; provider?: string; 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; yolo?: boolean }; data: AgentRecord };
621
622
  "agent:resume": { payload: { id: string; prompt?: string; surface?: AgentSurface; workspace?: string; tab?: string }; data: AgentRecord };
622
623
  "agent:get": { payload: { id: string }; data: AgentRecord };
623
624
  "agent:list": { payload: { repo?: string }; data: { agents: AgentRecord[] } };
@@ -712,7 +713,11 @@ export interface Commands {
712
713
  agent?: string;
713
714
  origin?: { surface?: string; tabId?: string; worktree?: string };
714
715
  };
715
- data: { id: string; presentation: "form" | "wait"; subject: string; supersededId: string | null };
716
+ /** `contextOmitted` appears only when the gate context plus every
717
+ question's `context` exceeded their shared 8192-byte budget: the
718
+ gate still opened, but question contexts were dropped, and the gate
719
+ context too when it was over the budget on its own. */
720
+ data: { id: string; presentation: "form" | "wait"; subject: string; supersededId: string | null; contextOmitted?: true };
716
721
  };
717
722
  /**
718
723
  * A CAS loss is a DEFINED OUTCOME, not an error: `ok:true` with
@@ -730,7 +735,10 @@ export interface Commands {
730
735
  "gate:answer": { payload: { id: string; answers: GateAnswer["answers"]; by: string; session?: string; override?: boolean }; data: { row: GateRow; conflict?: true } };
731
736
  /** `ok:false "not-found"` on an unknown id is terminal; the CLI loop must not re-enter on it.
732
737
  * `timeout` carries no row (nothing settled); `answered`/`closed` always carry the settled row. */
733
- "gate:wait": { payload: { id: string; waitMs?: number }; data: { status: "timeout" } | { status: "answered" | "closed"; row: GateRow } };
738
+ /** `sessionId` is the caller's own session: an answered result returned to
739
+ * the gate's nudged session is a recorded read and stamps `consumedAt`.
740
+ * Omitting it reads without consuming. */
741
+ "gate:wait": { payload: { id: string; waitMs?: number; sessionId?: string }; data: { status: "timeout" } | { status: "answered" | "closed"; row: GateRow } };
734
742
  /** Paged like events:list: an omitted `limit` clamps daemon-side rather than
735
743
  * forcing a full-table read; `cursor` is the paging rowid to resume from. */
736
744
  "gate:list": { payload: { open?: boolean; subjectPrefix?: string; kind?: string; limit?: number; cursor?: number }; data: { gates: GateRow[]; cursor: number } };
@@ -750,7 +758,7 @@ export interface Commands {
750
758
  "herd:list": { payload: { all?: boolean }; data: { herds: HerdListRow[] } };
751
759
  "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 } };
752
760
  /** `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. */
753
- "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 } };
761
+ "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; /** What the folder-trust check established: no modal was up, one was accepted and verified gone, one is still up (the job reads `stuck-at-modal`), or herdr could not be read. */ trust: "none" | "accepted" | "stuck" | "unchecked" } };
754
762
  "herd:gates": { payload: { herd: string }; data: { gates: GateRow[] } };
755
763
  "herd:ask": { payload: { herd: string; job: string; session: string; pane?: string; questions: GateQuestion[]; context?: string }; data: { gate: string } };
756
764
  "herd:milestone": { payload: { herd: string; job: string; session: string; pane?: string; artifact: string; summary?: string }; data: { gate: string; message: number } };
@@ -6,6 +6,9 @@ import type { GateOption, GateQuestion } from "./commands.ts";
6
6
  export interface GateOptionObject {
7
7
  value: string;
8
8
  label: string;
9
+ /** Per-option explanation, carried verbatim: never capitalized, never
10
+ suffixed, never filled in from value/label. */
11
+ description?: string;
9
12
  }
10
13
 
11
14
  export function gateOptionValue(o: GateOption): string {
@@ -45,9 +48,11 @@ function capitalize(label: string): string {
45
48
  word-like (see isWordLikeLabel), and an object form's `recommended:
46
49
  true` lifts into a " (Recommended)" label suffix -- guarded against
47
50
  double-appending -- since `recommended` itself does not survive into
48
- the returned object; the suffix IS its wire representation. Every
49
- returned entry is a full {value,label} pair -- callers may trust the
50
- return type without re-checking it. Pure and order-preserving. */
51
+ the returned object; the suffix IS its wire representation. An object
52
+ form's non-empty string `description` rides along untouched (no
53
+ capitalization, no suffix); any other description shape is dropped.
54
+ Every returned entry is a full {value,label} pair -- callers may trust
55
+ the return type without re-checking it. Pure and order-preserving. */
51
56
  export function normalizeGateOptions(options: GateOption[]): GateOptionObject[] {
52
57
  return options.map((o) => {
53
58
  const recommended = o !== null && typeof o === "object" && (o as { recommended?: unknown }).recommended === true;
@@ -70,10 +75,19 @@ export function normalizeGateOptions(options: GateOption[]): GateOptionObject[]
70
75
  // downstream `label || value` fallback (gate-kit) must still see label
71
76
  // as absent, not as a non-empty "(Recommended)" that hides the value.
72
77
  if (recommended && label && !HAS_RECOMMENDED_SUFFIX.test(label)) label += RECOMMENDED_SUFFIX;
73
- return { value, label };
78
+ const d = o !== null && typeof o === "object" ? (o as { description?: unknown }).description : undefined;
79
+ return typeof d === "string" && d ? { value, label, description: d } : { value, label };
74
80
  });
75
81
  }
76
82
 
83
+ /** Options normalize per normalizeGateOptions; a question's `context` is
84
+ kept verbatim when it is a non-blank string and dropped otherwise, so a
85
+ stored row never carries an empty or non-string context key. */
77
86
  export function normalizeGateQuestions(questions: GateQuestion[]): GateQuestion[] {
78
- return questions.map((q) => ({ ...q, options: normalizeGateOptions(q.options) }));
87
+ return questions.map((q) => {
88
+ const { context, ...rest } = q;
89
+ const out: GateQuestion = { ...rest, options: normalizeGateOptions(q.options) };
90
+ if (typeof context === "string" && context.trim()) out.context = context;
91
+ return out;
92
+ });
79
93
  }
@@ -666,35 +666,80 @@ export const REGISTRY: readonly SettingDef[] = [
666
666
  },
667
667
 
668
668
  // --- agent (rt agent handoff) --------------------------------------------
669
- // No defaults by design: an unset key means the flag is omitted from the
670
- // claude invocation entirely (spec "Settings").
669
+ // No defaults on any per-provider row, by design: an unset key means the
670
+ // flag is omitted from the launch entirely (spec "Settings"). agent.provider
671
+ // is the one exception -- it needs a concrete fallback to preserve
672
+ // claude-only behavior with zero code changes for callers who never set it.
671
673
  {
672
- key: "agent.model",
674
+ key: "agent.provider",
673
675
  type: "string",
674
676
  scopes: ["user", "machine"],
677
+ default: "claude",
675
678
  merge: "replace",
676
- description: "Default --model for rt agent launches; unset omits the flag.",
679
+ description: "Which provider rt agent start uses when --provider is not given: \"claude\" or \"codex\".",
677
680
  },
678
681
  {
679
- key: "agent.effort",
682
+ key: "agent.claude.model",
680
683
  type: "string",
681
684
  scopes: ["user", "machine"],
682
685
  merge: "replace",
683
- description: "Default --effort for rt agent launches; unset omits the flag.",
686
+ description: "Default --model for claude rt agent launches; unset omits the flag.",
684
687
  },
685
688
  {
686
- key: "agent.account",
689
+ key: "agent.claude.effort",
687
690
  type: "string",
688
691
  scopes: ["user", "machine"],
689
692
  merge: "replace",
690
- description: "cswap account email rt agent launches under; unset uses the default claude profile.",
693
+ description: "Default --effort for claude rt agent launches; unset omits the flag.",
691
694
  },
692
695
  {
693
- key: "agent.extraArgs",
696
+ key: "agent.claude.account",
694
697
  type: "string",
695
698
  scopes: ["user", "machine"],
696
699
  merge: "replace",
697
- description: "Opaque extra claude arguments appended to every rt agent launch (escape hatch).",
700
+ description: "cswap account email claude rt agent launches under; unset uses the default claude profile.",
701
+ },
702
+ {
703
+ key: "agent.claude.extraArgs",
704
+ type: "string",
705
+ scopes: ["user", "machine"],
706
+ merge: "replace",
707
+ description: "Opaque extra claude arguments appended to every claude rt agent launch (escape hatch).",
708
+ },
709
+ {
710
+ key: "agent.claude.yolo",
711
+ type: "boolean",
712
+ scopes: ["user", "machine"],
713
+ merge: "replace",
714
+ description: "Default --yolo (--dangerously-skip-permissions) for claude rt agent launches; unset behaves as false.",
715
+ },
716
+ {
717
+ key: "agent.codex.model",
718
+ type: "string",
719
+ scopes: ["user", "machine"],
720
+ merge: "replace",
721
+ description: "Default -m/--model for codex rt agent launches; unset omits the flag.",
722
+ },
723
+ {
724
+ key: "agent.codex.effort",
725
+ type: "string",
726
+ scopes: ["user", "machine"],
727
+ merge: "replace",
728
+ description: "Default reasoning effort for codex rt agent launches, passed as -c model_reasoning_effort=<value>; unset omits the override.",
729
+ },
730
+ {
731
+ key: "agent.codex.extraArgs",
732
+ type: "string",
733
+ scopes: ["user", "machine"],
734
+ merge: "replace",
735
+ description: "Opaque extra codex arguments appended to every codex rt agent launch (escape hatch).",
736
+ },
737
+ {
738
+ key: "agent.codex.yolo",
739
+ type: "boolean",
740
+ scopes: ["user", "machine"],
741
+ merge: "replace",
742
+ description: "Default --yolo (--dangerously-bypass-approvals-and-sandbox) for codex rt agent launches; unset behaves as false. codex has no per-agent account setting (see spec's Non-goals).",
698
743
  },
699
744
 
700
745
  // --- gates (escalation) ----------------------------------------------------