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