@mattstack/rt-client 0.16.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 +33 -0
- package/dist/commands.d.ts +305 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +206 -17
- package/dist/pane-ref.d.ts +8 -0
- package/dist/settings/paths.d.ts +8 -0
- package/dist/settings/team-local-read.d.ts +7 -0
- package/package.json +1 -1
- package/src/client.ts +163 -4
- package/src/commands.ts +90 -8
- package/src/index.ts +25 -0
- package/src/pane-ref.ts +33 -0
- package/src/settings/paths.ts +11 -0
- package/src/settings/registry-defs.ts +13 -3
- package/src/settings/team-local-read.ts +18 -0
- package/src/settings/write.ts +24 -3
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,25 @@ 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;
|
|
141
|
+
}
|
|
142
|
+
|
|
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 }
|
|
144
|
+
/** A herd row as `herd:list` reports it: the registry row plus how many jobs hang off it. */
|
|
145
|
+
export interface HerdListRow extends HerdInfo { jobs: number }
|
|
146
|
+
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 }
|
|
147
|
+
/** `lastGateStatus`/`lastGateDelivery` come from the job's `lastGate` row: an `answered` gate whose delivery is `dead-pane` is the "answered, worker not woken" case the shepherd must act on. */
|
|
148
|
+
export interface HerdStatusData {
|
|
149
|
+
herd: HerdInfo;
|
|
150
|
+
jobs: Array<HerdJobInfo & { openGate: string | null; paneStatus: string | null; lastGateStatus: GateStatus | null; lastGateDelivery: "delivered" | "dead-pane" | null }>;
|
|
151
|
+
unread: number;
|
|
152
|
+
lifecycleConnected: boolean;
|
|
153
|
+
hiddenUp: boolean | null;
|
|
154
|
+
/** The shepherd session's own `herd:<id>/` subscription row, or null when none is live. */
|
|
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"] };
|
|
135
158
|
}
|
|
136
159
|
|
|
137
160
|
/**
|
|
@@ -229,7 +252,10 @@ export interface InviteResult { paneId: string; delivered: "accepted" | "queued"
|
|
|
229
252
|
/** Duplicated shape on purpose: mirrors lib/daemon/inject.ts's InjectResult. */
|
|
230
253
|
export type PaneDelivery = "accepted" | "queued" | "refused";
|
|
231
254
|
export interface PaneSendResult { paneId: string; delivered: PaneDelivery; reason?: string }
|
|
232
|
-
|
|
255
|
+
/** `attendTab` is set only for a `bg:` ref: focus for a background pane IS
|
|
256
|
+
the attend flow (a visible tab running a terminal attach), and this is
|
|
257
|
+
that tab's id. */
|
|
258
|
+
export interface PaneFocusResult { paneId: string; focused: boolean; attendTab?: string }
|
|
233
259
|
|
|
234
260
|
// SKILLS-53: one judgment, computed once in rt, so the console and the tray
|
|
235
261
|
// never derive two verdicts that can disagree.
|
|
@@ -387,7 +413,9 @@ export interface WorktreeProvisionData {
|
|
|
387
413
|
export interface WorktreeCreateData { tree: string; path: string }
|
|
388
414
|
export interface WorktreeDisposeData {
|
|
389
415
|
disposed: string[];
|
|
390
|
-
|
|
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 }>;
|
|
391
419
|
recoverable: Array<{ tree: string; path: string; until: string }>;
|
|
392
420
|
}
|
|
393
421
|
export interface WorktreeRestoreData {
|
|
@@ -396,7 +424,7 @@ export interface WorktreeRestoreData {
|
|
|
396
424
|
export interface WorktreeFreshenData { ran: string[] }
|
|
397
425
|
export interface WorktreeAdoptData {
|
|
398
426
|
main: string; claimed: string[]; unmanaged: string[]; disposed: string[];
|
|
399
|
-
refused: Array<{ tree: string; reason: string }>;
|
|
427
|
+
refused: Array<{ tree: string; reason: string; detail?: string }>;
|
|
400
428
|
}
|
|
401
429
|
|
|
402
430
|
/** Duplicated shape on purpose: mirrors lib/endpoint/store.ts's EndpointClaim. */
|
|
@@ -540,7 +568,7 @@ export interface Commands {
|
|
|
540
568
|
"chat:dm-open": { payload: { from: string; to: string; sessionId?: string }; data: { room: string; created: boolean } };
|
|
541
569
|
|
|
542
570
|
// ─── Agent handoff (rt agent) ────────────────────────────────────────────
|
|
543
|
-
"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 }; data: AgentRecord };
|
|
571
|
+
"agent:start": { payload: { repo: string; cwd: string; prompt?: string; surface?: AgentSurface; model?: string; effort?: string; account?: string; label?: string; caller?: string; workspace?: string; tab?: string; extraArgs?: string; env?: Record<string, string>; herdrSocket?: string; handle?: string; bg?: boolean }; data: AgentRecord };
|
|
544
572
|
"agent:resume": { payload: { id: string; prompt?: string; surface?: AgentSurface; workspace?: string; tab?: string }; data: AgentRecord };
|
|
545
573
|
"agent:get": { payload: { id: string }; data: AgentRecord };
|
|
546
574
|
"agent:list": { payload: { repo?: string }; data: { agents: AgentRecord[] } };
|
|
@@ -554,7 +582,9 @@ export interface Commands {
|
|
|
554
582
|
data: { pane: ChatPane; ready: boolean };
|
|
555
583
|
};
|
|
556
584
|
"pane:send": { payload: { paneId: string; text: string; callerPane?: string }; data: PaneSendResult };
|
|
557
|
-
|
|
585
|
+
/** `callerWorkspace` (HERDR_WORKSPACE_ID) is required only for a `bg:`
|
|
586
|
+
ref, whose focus opens an attend tab in the caller's own workspace. */
|
|
587
|
+
"pane:focus": { payload: { paneId: string; callerWorkspace?: string }; data: PaneFocusResult };
|
|
558
588
|
|
|
559
589
|
// ─── R013/R016 ────────────────────────────────────────────────
|
|
560
590
|
"cache:read": { payload: { branches?: string[]; maxAgeMs?: number; repoIdentity?: string }; data: Record<string, BranchEnrichment> };
|
|
@@ -596,8 +626,15 @@ export interface Commands {
|
|
|
596
626
|
* `conflict:true` and the WINNING row, so every consumer gets the winner
|
|
597
627
|
* typed with no envelope hacks. `ok:false` is reserved for
|
|
598
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?}`.
|
|
599
636
|
*/
|
|
600
|
-
"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 } };
|
|
601
638
|
/** `ok:false "not-found"` on an unknown id is terminal; the CLI loop must not re-enter on it.
|
|
602
639
|
* `timeout` carries no row (nothing settled); `answered`/`closed` always carry the settled row. */
|
|
603
640
|
"gate:wait": { payload: { id: string; waitMs?: number }; data: { status: "timeout" } | { status: "answered" | "closed"; row: GateRow } };
|
|
@@ -606,12 +643,31 @@ export interface Commands {
|
|
|
606
643
|
"gate:list": { payload: { open?: boolean; subjectPrefix?: string; kind?: string; limit?: number; cursor?: number }; data: { gates: GateRow[]; cursor: number } };
|
|
607
644
|
"gate:park": { payload: { id: string }; data: { ok: true } };
|
|
608
645
|
"gate:close": { payload: { id: string; reason: "abandoned" | "superseded" | "pruned" }; data: { ok: true } };
|
|
609
|
-
"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 } };
|
|
610
647
|
"gate:unsubscribe": { payload: { id: string }; data: { removed: boolean } };
|
|
611
648
|
/** The shepherd's gap-recovery liveness check and the observability window
|
|
612
649
|
* onto delivery outcomes (dead marks included). */
|
|
613
650
|
"gate:subscriptions": { payload: { session?: string; live?: boolean }; data: { subscriptions: GateSubscription[] } };
|
|
614
651
|
|
|
652
|
+
// ─── Herd (shepherd run registry) ────────────────────────────────────────
|
|
653
|
+
"herd:start": { payload: { name: string; repo: string; session: string; hidden?: boolean }; data: { herd: string; room: string; workspace: string; subscription: string; handle: string; hidden: boolean } };
|
|
654
|
+
"herd:resume": { payload: { herd: string; session: string }; data: { subscription: string; gates: GateRow[]; unread: number; status: HerdStatusData; handle: string } };
|
|
655
|
+
"herd:status": { payload: { herd: string }; data: HerdStatusData };
|
|
656
|
+
/** Active herds only unless `all`, so a shepherd's "which herd am I on" question has one answer. */
|
|
657
|
+
"herd:list": { payload: { all?: boolean }; data: { herds: HerdListRow[] } };
|
|
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 } };
|
|
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. */
|
|
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 } };
|
|
661
|
+
"herd:gates": { payload: { herd: string }; data: { gates: GateRow[] } };
|
|
662
|
+
"herd:ask": { payload: { herd: string; job: string; session: string; pane?: string; questions: GateQuestion[]; context?: string }; data: { gate: string } };
|
|
663
|
+
"herd:milestone": { payload: { herd: string; job: string; session: string; pane?: string; artifact: string; summary?: string }; data: { gate: string; message: number } };
|
|
664
|
+
"herd:answer": { payload: { gate: string }; data: { gate: string; status: GateStatus; answer: GateAnswer | null; closedReason: GateRow["closedReason"] } };
|
|
665
|
+
"herd:report": { payload: { herd: string; job: string; body: string }; data: { message: number } };
|
|
666
|
+
/** `callerWorkspace` is the attending session's own HERDR_WORKSPACE_ID: the attached tab opens there, not in the herd's workspace. */
|
|
667
|
+
"herd:attend": { payload: { herd: string; job: string; callerWorkspace: string }; data: { tab: string; pane: string } };
|
|
668
|
+
"herd:stop-hidden": { payload: Record<string, never>; data: { stopped: boolean } };
|
|
669
|
+
"herd:wrap-up": { payload: { herd: string; closePanes?: boolean; dispose?: string[]; deleteJobDirs?: boolean; archiveRoom?: boolean }; data: { closed: string[]; workspaceClosed: boolean; disposed: string[]; refused: Array<{ tree: string; reason: string }>; deletedJobDirs: boolean; archived: boolean } };
|
|
670
|
+
|
|
615
671
|
/** Wire reply on success is always `{ok:true, repaired}` (no `data`
|
|
616
672
|
* wrapper) — `data` here documents the extra field the same way PingData
|
|
617
673
|
* does for `ping`, not the literal wire nesting (R3). */
|
|
@@ -632,6 +688,13 @@ export interface Commands {
|
|
|
632
688
|
"worktree:restore": { payload: { repoName: string; tree: string }; data: WorktreeRestoreData };
|
|
633
689
|
"worktree:freshen": { payload: { repoName?: string; tree?: string }; data: WorktreeFreshenData };
|
|
634
690
|
"worktree:adopt": { payload: { repoName: string; claim?: boolean }; data: WorktreeAdoptData };
|
|
691
|
+
|
|
692
|
+
// ─── Background server (daemon-owned background herdr session) ──────────
|
|
693
|
+
"bg:ensure": { payload: { claim?: string }; data: { socket: string; started: boolean; parity: { ok: boolean; drift: string[] } | null } };
|
|
694
|
+
"bg:status": { payload: Record<string, never>; data: { up: boolean; socket: string; claims: Array<{ owner: string; pane: string | null; createdAt: number }> } };
|
|
695
|
+
/** Rejects (`ok:false`) naming every live claim owner while any claim is held. */
|
|
696
|
+
"bg:stop": { payload: Record<string, never>; data: { stopped: boolean } };
|
|
697
|
+
"bg:release": { payload: { claim: string }; data: { released: boolean } };
|
|
635
698
|
}
|
|
636
699
|
|
|
637
700
|
export type CommandName = keyof Commands;
|
|
@@ -715,6 +778,20 @@ export const COMMAND_NAMES: readonly CommandName[] = [
|
|
|
715
778
|
"gate:subscribe",
|
|
716
779
|
"gate:unsubscribe",
|
|
717
780
|
"gate:subscriptions",
|
|
781
|
+
"herd:start",
|
|
782
|
+
"herd:resume",
|
|
783
|
+
"herd:status",
|
|
784
|
+
"herd:list",
|
|
785
|
+
"herd:close",
|
|
786
|
+
"herd:spawn",
|
|
787
|
+
"herd:gates",
|
|
788
|
+
"herd:ask",
|
|
789
|
+
"herd:milestone",
|
|
790
|
+
"herd:answer",
|
|
791
|
+
"herd:report",
|
|
792
|
+
"herd:attend",
|
|
793
|
+
"herd:stop-hidden",
|
|
794
|
+
"herd:wrap-up",
|
|
718
795
|
"hooks:repair",
|
|
719
796
|
"hooks:watch",
|
|
720
797
|
"sdm:catalog",
|
|
@@ -729,4 +806,9 @@ export const COMMAND_NAMES: readonly CommandName[] = [
|
|
|
729
806
|
"worktree:restore",
|
|
730
807
|
"worktree:freshen",
|
|
731
808
|
"worktree:adopt",
|
|
809
|
+
|
|
810
|
+
"bg:ensure",
|
|
811
|
+
"bg:status",
|
|
812
|
+
"bg:stop",
|
|
813
|
+
"bg:release",
|
|
732
814
|
];
|
package/src/index.ts
CHANGED
|
@@ -54,6 +54,24 @@ export {
|
|
|
54
54
|
gateSubscribe,
|
|
55
55
|
gateUnsubscribe,
|
|
56
56
|
gateSubscriptions,
|
|
57
|
+
herdStart,
|
|
58
|
+
herdSpawn,
|
|
59
|
+
herdAsk,
|
|
60
|
+
herdMilestone,
|
|
61
|
+
herdAnswer,
|
|
62
|
+
herdReport,
|
|
63
|
+
herdGates,
|
|
64
|
+
herdStatus,
|
|
65
|
+
herdList,
|
|
66
|
+
herdResume,
|
|
67
|
+
herdClose,
|
|
68
|
+
herdAttend,
|
|
69
|
+
herdWrapUp,
|
|
70
|
+
herdStopHidden,
|
|
71
|
+
bgEnsure,
|
|
72
|
+
bgStatus,
|
|
73
|
+
bgStop,
|
|
74
|
+
bgRelease,
|
|
57
75
|
} from "./client.ts";
|
|
58
76
|
|
|
59
77
|
export { COMMAND_NAMES, GATE_BY_PANE, gateOptionValue, gateOptionLabel } from "./commands.ts";
|
|
@@ -100,6 +118,10 @@ export type {
|
|
|
100
118
|
GateAnswer,
|
|
101
119
|
GateRow,
|
|
102
120
|
GateSubscription,
|
|
121
|
+
HerdInfo,
|
|
122
|
+
HerdListRow,
|
|
123
|
+
HerdJobInfo,
|
|
124
|
+
HerdStatusData,
|
|
103
125
|
} from "./commands.ts";
|
|
104
126
|
|
|
105
127
|
export { subscribe, createRelay, DEFAULT_WS_URL } from "./relay.ts";
|
|
@@ -112,6 +134,9 @@ export { repoNameForPath } from "./repos.ts";
|
|
|
112
134
|
export { decidePlacement, openSmartPane } from "./smart-pane.ts";
|
|
113
135
|
export type { Placement, PlacementOpts, HerdrCall } from "./smart-pane.ts";
|
|
114
136
|
|
|
137
|
+
export { BG_PREFIX, parsePaneRef, formatPaneRef } from "./pane-ref.ts";
|
|
138
|
+
export type { PaneServer, PaneRef } from "./pane-ref.ts";
|
|
139
|
+
|
|
115
140
|
// ─── Settings (RT-50) ────────────────────────────────────────────────────────
|
|
116
141
|
|
|
117
142
|
export { getSetting, listSettings, explainSetting, expandVariables, SCOPE_ORDER, setSettingsWarnSink } from "./settings/resolve.ts";
|
package/src/pane-ref.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const BG_PREFIX = "bg:";
|
|
2
|
+
|
|
3
|
+
export type PaneServer = "visible" | "bg";
|
|
4
|
+
|
|
5
|
+
export interface PaneRef {
|
|
6
|
+
server: PaneServer;
|
|
7
|
+
paneId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parsePaneRef(ref: string): PaneRef {
|
|
11
|
+
if (ref.startsWith(BG_PREFIX)) {
|
|
12
|
+
return {
|
|
13
|
+
server: "bg",
|
|
14
|
+
paneId: ref.slice(BG_PREFIX.length),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
server: "visible",
|
|
19
|
+
paneId: ref,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatPaneRef(paneId: string, server: PaneServer): string {
|
|
24
|
+
if (server === "visible") {
|
|
25
|
+
return paneId;
|
|
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
|
+
}
|
|
32
|
+
return BG_PREFIX + paneId;
|
|
33
|
+
}
|
package/src/settings/paths.ts
CHANGED
|
@@ -27,6 +27,17 @@ export function teamSettingsPath(team: string): string {
|
|
|
27
27
|
return join(teamsDir(), team, "mattstack", "settings.team.jsonc");
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* ~/.mattstack/rt/teams/<team>.json: the machine-local team record. Mirrored
|
|
32
|
+
* from repo-tools/lib/team/team-local.ts's teamLocalPath, which is the
|
|
33
|
+
* authority: rt-client has no dependency on rt's lib/, so this literal is
|
|
34
|
+
* duplicated here rather than imported (same convention as `teamSettingsPath`
|
|
35
|
+
* and `userSettingsPath` above).
|
|
36
|
+
*/
|
|
37
|
+
export function teamLocalPath(team: string): string {
|
|
38
|
+
return join(home(), ".mattstack", "rt", "teams", `${team}.json`);
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
/**
|
|
31
42
|
* ~/.mattstack/user/local/<machineKey()>/settings.local.jsonc — the machine
|
|
32
43
|
* store, TRACKED and keyed per machine (path literals legal here only).
|
|
@@ -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}]. pattern is matched against the events-bus topic (Bun.Glob semantics); title/message may interpolate `{field}` from the event payload. 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
|
|
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
|
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One field of the machine-local team record, for the write guard. The record
|
|
3
|
+
* itself is owned by repo-tools/lib/team/team-local.ts; this reads only what
|
|
4
|
+
* the guard needs and never writes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
import { teamLocalPath } from "./paths.ts";
|
|
9
|
+
|
|
10
|
+
/** Unreadable, absent or malformed all read as false, so nothing that predates the field is refused. */
|
|
11
|
+
export function isJoinedTeam(team: string): boolean {
|
|
12
|
+
try {
|
|
13
|
+
const parsed: unknown = JSON.parse(readFileSync(teamLocalPath(team), "utf8"));
|
|
14
|
+
return typeof parsed === "object" && parsed !== null && (parsed as { joinedByRt?: unknown }).joinedByRt === true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/settings/write.ts
CHANGED
|
@@ -100,6 +100,7 @@ import { dirname } from "path";
|
|
|
100
100
|
import { machineSettingsPath, teamSettingsPath, userSettingsPath } from "./paths.ts";
|
|
101
101
|
import { getDef, isMigrated, validateValue, type SettingDef, type SettingScope } from "./registry-machinery.ts";
|
|
102
102
|
import { listTeams } from "./stores.ts";
|
|
103
|
+
import { isJoinedTeam } from "./team-local-read.ts";
|
|
103
104
|
|
|
104
105
|
export interface SetSettingOpts {
|
|
105
106
|
/** Normalized repo identity — required to target a repoScoped key's `repos.<identity>` section. */
|
|
@@ -208,6 +209,19 @@ function migratedFalseMessage(key: string, def: SettingDef): string {
|
|
|
208
209
|
return `"${key}" is not writable through the settings resolver yet${legacyPart}`;
|
|
209
210
|
}
|
|
210
211
|
|
|
212
|
+
/**
|
|
213
|
+
* A clone that arrived by redeeming an invite is pull-only, so a write here
|
|
214
|
+
* would never reach the team AND would leave a tracked file dirty, which is
|
|
215
|
+
* enough on its own to make the daemon's fast-forward pull fail.
|
|
216
|
+
*/
|
|
217
|
+
function refuseIfJoined(team: string): void {
|
|
218
|
+
if (isJoinedTeam(team)) {
|
|
219
|
+
refuse(
|
|
220
|
+
`this machine joined "${team}" by invite, so its clone is pull-only and team settings cannot be written here. Ask the team's owner to make this change. Member-proposed changes are tracked in MAT-415.`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
211
225
|
/** Resolves which store file a write targets, applying the team-selection rule for `scope: "team"`. */
|
|
212
226
|
function resolveStorePath(scope: SettingScope, opts: SetSettingOpts): string {
|
|
213
227
|
if (scope === "user") return userSettingsPath();
|
|
@@ -218,6 +232,7 @@ function resolveStorePath(scope: SettingScope, opts: SetSettingOpts): string {
|
|
|
218
232
|
if (!existsSync(path)) {
|
|
219
233
|
refuse(`team store for "${opts.team}" does not exist (${path}) — clone/seed it before writing to it`);
|
|
220
234
|
}
|
|
235
|
+
refuseIfJoined(opts.team);
|
|
221
236
|
return path;
|
|
222
237
|
}
|
|
223
238
|
|
|
@@ -228,7 +243,9 @@ function resolveStorePath(scope: SettingScope, opts: SetSettingOpts): string {
|
|
|
228
243
|
if (teams.length > 1) {
|
|
229
244
|
refuse(`multiple local team stores found (${teams.join(", ")}) — pass opts.team to choose one`);
|
|
230
245
|
}
|
|
231
|
-
|
|
246
|
+
const team = teams[0] as string;
|
|
247
|
+
refuseIfJoined(team);
|
|
248
|
+
return teamSettingsPath(team);
|
|
232
249
|
}
|
|
233
250
|
|
|
234
251
|
/**
|
|
@@ -243,7 +260,9 @@ function resolveStorePathForUnset(scope: SettingScope, opts: SetSettingOpts): st
|
|
|
243
260
|
|
|
244
261
|
if (opts.team !== undefined) {
|
|
245
262
|
const path = teamSettingsPath(opts.team);
|
|
246
|
-
|
|
263
|
+
if (!existsSync(path)) return null;
|
|
264
|
+
refuseIfJoined(opts.team);
|
|
265
|
+
return path;
|
|
247
266
|
}
|
|
248
267
|
|
|
249
268
|
const teams = listTeams();
|
|
@@ -251,7 +270,9 @@ function resolveStorePathForUnset(scope: SettingScope, opts: SetSettingOpts): st
|
|
|
251
270
|
if (teams.length > 1) {
|
|
252
271
|
refuse(`multiple local team stores found (${teams.join(", ")}) — pass opts.team to choose one`);
|
|
253
272
|
}
|
|
254
|
-
|
|
273
|
+
const team = teams[0] as string;
|
|
274
|
+
refuseIfJoined(team);
|
|
275
|
+
return teamSettingsPath(team);
|
|
255
276
|
}
|
|
256
277
|
|
|
257
278
|
/** `// header comment\n{}\n` — see module doc for why the object must be seeded before the first `modify`. */
|