@mattstack/rt-client 0.27.0 → 0.29.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.
@@ -27,6 +27,17 @@ export interface ProjectMRsScope {
27
27
  daemon or before the first sweep that demanded a section. */
28
28
  knownSections?: string[];
29
29
  }
30
+ /** Classified cause of a repo's failing project sync (lib/daemon/project-sync-health.ts). */
31
+ export type ProjectSyncErrorKind = "rate-limited" | "auth" | "server-error" | "timeout" | "other";
32
+ /** A repo's current unbroken run of failed project syncs. */
33
+ export interface ProjectSyncError {
34
+ /** First failure of the run; later failures leave it alone. */
35
+ since: number;
36
+ lastAt: number;
37
+ kind: ProjectSyncErrorKind;
38
+ /** Raw error text, capped at 200 chars. */
39
+ message: string;
40
+ }
30
41
  export interface ProjectMRsData {
31
42
  mrs: Record<string, {
32
43
  pr: PullRequest;
@@ -37,6 +48,8 @@ export interface ProjectMRsData {
37
48
  source: "poll" | "events" | "mutation";
38
49
  syncedAt: number;
39
50
  scope?: ProjectMRsScope;
51
+ /** Present while this repo's most recent project sync failed. */
52
+ syncError?: ProjectSyncError;
40
53
  }
41
54
  export interface DiscussionsData {
42
55
  discussions: Discussion[];
@@ -247,7 +260,11 @@ export interface HerdStatusData {
247
260
  sessionDead: boolean | null;
248
261
  lastGateStatus: GateStatus | null;
249
262
  lastGateDelivery: "delivered" | "dead-pane" | "confirmed" | "stuck" | null;
250
- lastGateConsumed: boolean | null;
263
+ lastGateConsumed: boolean | null; /** The daemon watchdog's escalation ladder for this job: strikes so far and when it last acted. Absent when the job is not on the ladder (healthy, no watchdog wired, or the daemon restarted since). */
264
+ watchdog?: {
265
+ strikes: number;
266
+ lastPokeAt: number | null;
267
+ };
251
268
  }>;
252
269
  unread: number;
253
270
  lifecycleConnected: boolean;
@@ -486,6 +503,10 @@ export interface ReconcilerStatus {
486
503
  herdrReachable: boolean;
487
504
  executors: ExecutorView[];
488
505
  }
506
+ /** What a claude spawn's folder-trust check established, when one ran:
507
+ `none` = no dialog was up, `accepted` = one was answered, `stuck` = one is
508
+ still up and needs a human, `unchecked` = nothing could be established. */
509
+ export type TrustOutcome = "none" | "accepted" | "stuck" | "unchecked";
489
510
  export interface AgentRecord {
490
511
  id: string;
491
512
  repo: string;
@@ -683,10 +704,14 @@ export interface WorktreeProvisionData {
683
704
  branchState: "new" | "tracking-remote" | "existing-clean" | "diverged" | "behind";
684
705
  readyFailed?: true;
685
706
  failedStep?: string;
707
+ /** Set only when the tree was built by hydrating from the repo's golden donor, not the on-deck pool or a cold create. */
708
+ hydratedFrom?: string;
686
709
  }
687
710
  export interface WorktreeCreateData {
688
711
  tree: string;
689
712
  path: string;
713
+ /** Set only for `--on-deck`, and only when that tree hydrated from the golden rather than cold-creating. */
714
+ hydratedFrom?: string;
690
715
  }
691
716
  export interface WorktreeDisposeData {
692
717
  disposed: string[];
@@ -779,6 +804,28 @@ export interface DiscussionsDiffsData {
779
804
  truncated: boolean;
780
805
  }
781
806
  export type MRActionName = "merge" | "rebase" | "approve" | "unapprove" | "setAutoMerge" | "cancelAutoMerge" | "retryJob" | "retryPipeline" | "toggleDraft" | "requestReReview";
807
+ /** One worktree's git badge as the daemon sweep computed it. All timestamps ISO 8601. */
808
+ export interface GitWorktreeBadge {
809
+ worktree: string;
810
+ branch: string | null;
811
+ detached: boolean;
812
+ staged: number;
813
+ unstaged: number;
814
+ untracked: number;
815
+ conflicted: number;
816
+ clean: boolean;
817
+ ahead: number | null;
818
+ behind: number | null;
819
+ upstream: string | null;
820
+ lastFetchedAt: string | null;
821
+ updatedAt: string;
822
+ }
823
+ /** repo is the serialized identity (the repo-index key). error is set when the last sweep could not read the repo; stale worktrees may accompany it. */
824
+ export interface RepoStatusRow {
825
+ repo: string;
826
+ worktrees: GitWorktreeBadge[];
827
+ error: string | null;
828
+ }
782
829
  export interface Commands {
783
830
  "project-mrs:read": {
784
831
  payload: {
@@ -1160,9 +1207,12 @@ export interface Commands {
1160
1207
  handle?: string;
1161
1208
  bg?: boolean;
1162
1209
  subject?: string;
1163
- yolo?: boolean;
1210
+ yolo?: boolean; /** How long the folder-trust check lets the dialog paint before reading the screen; interactive launches keep the short default, herd:spawn passes its own longer budget. */
1211
+ trustWaitMs?: number;
1212
+ };
1213
+ data: AgentRecord & {
1214
+ trust?: TrustOutcome;
1164
1215
  };
1165
- data: AgentRecord;
1166
1216
  };
1167
1217
  "agent:resume": {
1168
1218
  payload: {
@@ -1445,6 +1495,15 @@ export interface Commands {
1445
1495
  };
1446
1496
  data: unknown;
1447
1497
  };
1498
+ "repos:status": {
1499
+ payload: {
1500
+ refresh?: boolean;
1501
+ };
1502
+ data: {
1503
+ repos: RepoStatusRow[];
1504
+ sweptAt: string | null;
1505
+ };
1506
+ };
1448
1507
  "freshness:reconcile": {
1449
1508
  payload: Record<string, never>;
1450
1509
  data: unknown;
@@ -1513,13 +1572,23 @@ export interface Commands {
1513
1572
  /** `contextOmitted` appears only when the gate context plus every
1514
1573
  question's `context` exceeded their shared 8192-byte budget: the
1515
1574
  gate still opened, but question contexts were dropped, and the gate
1516
- context too when it was over the budget on its own. */
1575
+ context too when it was over the budget on its own.
1576
+ `formCapExceeded`/`formCapAdvisory` appear only when the pane could
1577
+ have presented an in-pane form and one or more questions exceeded
1578
+ its 4-option cap, which is the one thing that forced this gate to
1579
+ `wait`: the gate still opens, and the advisory names the structural
1580
+ fix so the caller can re-author. */
1517
1581
  data: {
1518
1582
  id: string;
1519
1583
  presentation: "form" | "wait";
1520
1584
  subject: string;
1521
1585
  supersededId: string | null;
1522
1586
  contextOmitted?: true;
1587
+ formCapExceeded?: Array<{
1588
+ question: string;
1589
+ options: number;
1590
+ }>;
1591
+ formCapAdvisory?: string;
1523
1592
  };
1524
1593
  };
1525
1594
  /**
@@ -1634,6 +1703,7 @@ export interface Commands {
1634
1703
  repo: string;
1635
1704
  session: string;
1636
1705
  hidden?: boolean;
1706
+ callerPane?: string;
1637
1707
  };
1638
1708
  data: {
1639
1709
  herd: string;
@@ -1648,6 +1718,7 @@ export interface Commands {
1648
1718
  payload: {
1649
1719
  herd: string;
1650
1720
  session: string;
1721
+ callerPane?: string;
1651
1722
  };
1652
1723
  data: {
1653
1724
  subscription: string;
package/dist/gate.js CHANGED
@@ -99,12 +99,12 @@ function gatePresentation(args) {
99
99
  return args.questions.every((q) => q.options.length <= GATE_FORM_OPTION_CAP) ? "form" : "wait";
100
100
  }
101
101
  export {
102
- validateGateAnswers,
103
- unwrapGateAnswerValue,
104
- normalizeGateQuestions,
105
- normalizeGateOptions,
106
- gatePresentation,
107
- gateOptionValue,
102
+ GATE_FORM_OPTION_CAP,
108
103
  gateOptionLabel,
109
- GATE_FORM_OPTION_CAP
104
+ gateOptionValue,
105
+ gatePresentation,
106
+ normalizeGateOptions,
107
+ normalizeGateQuestions,
108
+ unwrapGateAnswerValue,
109
+ validateGateAnswers
110
110
  };
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export { normalizeGateOptions, normalizeGateQuestions } from "./gate-options.ts"
8
8
  export type { GateOptionObject } from "./gate-options.ts";
9
9
  export { unwrapGateAnswerValue, validateGateAnswers } from "./gate-answers.ts";
10
10
  export type { GateAnswerWire } from "./gate-answers.ts";
11
- 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";
11
+ export type { Discussion, DemandDecl, ProjectMRsScope, ProjectSyncError, ProjectSyncErrorKind, 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";
12
12
  export { subscribe, createRelay, DEFAULT_WS_URL } from "./relay.ts";
13
13
  export type { RelayEventType } from "./relay.ts";
14
14
  export { daemonHealth } from "./health.ts";
package/dist/index.js CHANGED
@@ -382,6 +382,8 @@ function herdStart(a, o = {}) {
382
382
  const payload = { name: a.name, repo: a.repo, session: a.session };
383
383
  if (a.hidden !== undefined)
384
384
  payload.hidden = a.hidden;
385
+ if (a.callerPane !== undefined)
386
+ payload.callerPane = a.callerPane;
385
387
  return rtCommand("herd:start", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 60000 });
386
388
  }
387
389
  function herdSpawn(a, o = {}) {
@@ -425,7 +427,10 @@ function herdList(a = {}, o = {}) {
425
427
  return rtCommand("herd:list", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
426
428
  }
427
429
  function herdResume(a, o = {}) {
428
- return rtCommand("herd:resume", { herd: a.herd, session: a.session }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
430
+ const payload = { herd: a.herd, session: a.session };
431
+ if (a.callerPane !== undefined)
432
+ payload.callerPane = a.callerPane;
433
+ return rtCommand("herd:resume", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
429
434
  }
430
435
  function herdClose(a, o = {}) {
431
436
  return rtCommand("herd:close", { herd: a.herd, job: a.job }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30000 });
@@ -577,6 +582,7 @@ var COMMAND_NAMES = [
577
582
  "endpoint:release",
578
583
  "endpoint:status",
579
584
  "repos:locate",
585
+ "repos:status",
580
586
  "freshness:reconcile",
581
587
  "reconciler:status",
582
588
  "reconciler:clear",
@@ -1115,6 +1121,16 @@ var REGISTRY = [
1115
1121
  migrated: true,
1116
1122
  description: "Age floor in days for the log janitor pruning every surface's rotated log files under ~/.mattstack/rt/logs (default 14). A fresh key, not an ownership-latch port, so a default is fine here."
1117
1123
  },
1124
+ {
1125
+ key: "rt.gitStatus",
1126
+ type: "object",
1127
+ scopes: ALL_SCOPES,
1128
+ default: { sweep: true, sweepIntervalSec: 300, fetchIntervalSec: 900 },
1129
+ merge: "deep",
1130
+ repoScoped: true,
1131
+ migrated: true,
1132
+ description: "Mission-control git badge sweep. sweep gates the daemon sweep and fetchIntervalSec (the background fetch cadence, 0 disables fetching) are both read per repo, so a per-repo override of either takes effect (a repo override of { sweep: false } opts that repo out). sweepIntervalSec, the minimum seconds between sweeps, is read only from the global config by the sweep tick; a per-repo override of sweepIntervalSec has no effect."
1133
+ },
1118
1134
  {
1119
1135
  key: "rt.logLevel",
1120
1136
  type: "string",
@@ -1605,6 +1621,86 @@ var REGISTRY = [
1605
1621
  default: 10,
1606
1622
  merge: "replace",
1607
1623
  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.`
1624
+ },
1625
+ {
1626
+ key: "herd.watchdog.enabled",
1627
+ type: "boolean",
1628
+ scopes: ["machine"],
1629
+ default: true,
1630
+ merge: "replace",
1631
+ description: "Turns the herd watchdog sweep on."
1632
+ },
1633
+ {
1634
+ key: "herd.watchdog.fastMins",
1635
+ type: "number",
1636
+ scopes: ["machine"],
1637
+ default: 2,
1638
+ merge: "replace",
1639
+ description: "Idle minutes before a worker with a pending DM, mention, or answered gate is poked."
1640
+ },
1641
+ {
1642
+ key: "herd.watchdog.shepherdFastMins",
1643
+ type: "number",
1644
+ scopes: ["machine"],
1645
+ default: 5,
1646
+ merge: "replace",
1647
+ description: "Minutes an open human-owned gate in the herd may stay unanswered before the shepherd is poked. Unread DMs and mentions for the shepherd wedge immediately and are not delayed by this key."
1648
+ },
1649
+ {
1650
+ key: "herd.watchdog.backstopMins",
1651
+ type: "number",
1652
+ scopes: ["machine"],
1653
+ default: 15,
1654
+ merge: "replace",
1655
+ description: "Idle minutes before any non-gated active worker is poked."
1656
+ },
1657
+ {
1658
+ key: "herd.watchdog.retryMins",
1659
+ type: "number",
1660
+ scopes: ["machine"],
1661
+ default: 5,
1662
+ merge: "replace",
1663
+ description: "Minutes between escalation strikes while the watchdog keeps poking an unresponsive worker."
1664
+ },
1665
+ {
1666
+ key: "herd.watchdog.notifyQuietMins",
1667
+ type: "number",
1668
+ scopes: ["machine"],
1669
+ default: 30,
1670
+ merge: "replace",
1671
+ description: "Minimum minutes between macOS notifications for the same herd."
1672
+ },
1673
+ {
1674
+ key: "herd.watchdog.nagMins",
1675
+ type: "number",
1676
+ scopes: ["machine"],
1677
+ default: 30,
1678
+ merge: "replace",
1679
+ description: "Minutes a done job may hold its pane before the shepherd is nagged to reclaim it."
1680
+ },
1681
+ {
1682
+ key: "herd.watchdog.notifyHuman",
1683
+ type: "boolean",
1684
+ scopes: ["machine"],
1685
+ default: true,
1686
+ merge: "replace",
1687
+ description: "Whether the watchdog's top escalation rung sends a macOS notification to the human."
1688
+ },
1689
+ {
1690
+ key: "herd.watchdog.midRunTrustAccept",
1691
+ type: "boolean",
1692
+ scopes: ["machine"],
1693
+ default: false,
1694
+ merge: "replace",
1695
+ description: "Whether the watchdog may drive a mid-run folder-trust dialog on a daemon-provisioned tree itself, rather than only parking the job and notifying (RT-196). Off by default: the screen match cannot yet confirm the dialog's folder matches the job's worktree, so a working session showing an unrelated permission prompt with the same shape is a real risk."
1696
+ },
1697
+ {
1698
+ key: "panes.relocationAutoAccept",
1699
+ type: "boolean",
1700
+ scopes: ["machine"],
1701
+ default: true,
1702
+ merge: "replace",
1703
+ description: "Whether the daemon may answer Claude Code's EnterWorktree permission-root relocation prompt on a blocked pane by itself (RT-200). Unlike the trust dialog, the prompt names the worktree path in its own body, and the daemon accepts only when that exact path is in rt's worktree registry, so this is on by default. Read by both the herd watchdog and the executor reconciler."
1608
1704
  }
1609
1705
  ];
1610
1706
 
@@ -1779,11 +1875,11 @@ function expandString(input, ctx) {
1779
1875
  return match;
1780
1876
  });
1781
1877
  }
1782
- function teamPath(teamsDir2, name) {
1878
+ function teamPath(teamsDir, name) {
1783
1879
  if (name.includes("/") || name.includes("\\") || name.includes("..")) {
1784
1880
  throw new Error(`rt: cannot expand \${team:${name}} — a team name must be a single directory segment (no "/", "\\" or "..")`);
1785
1881
  }
1786
- return join6(teamsDir2, name);
1882
+ return join6(teamsDir, name);
1787
1883
  }
1788
1884
  function required(value, name, needs) {
1789
1885
  if (value === undefined || value === "") {
@@ -2448,122 +2544,122 @@ async function resolveNameToIdentity(name, reposJsonPath) {
2448
2544
  }
2449
2545
  }
2450
2546
  export {
2451
- validateValue,
2452
- validateGateAnswers,
2453
- unwrapGateAnswerValue,
2454
- unsetSetting,
2455
- subscribe,
2456
- setSettingsWarnSink,
2457
- setSetting,
2458
- serializeIdentity,
2459
- rtCommand,
2460
- resolveNameToIdentity,
2461
- resolveForgeToken,
2462
- repoNameForPath,
2463
- reconcilerStatus,
2464
- reconcilerClear,
2465
- readStore,
2466
- readProjectMRs,
2467
- readMrsByBranch,
2468
- readDiscussions,
2469
- readBranchCache,
2470
- parsePaneRef,
2471
- parseIdentity,
2472
- paneSpawn,
2473
- paneSend,
2474
- panePeek,
2475
- paneList,
2476
- paneFocus,
2477
- paneDirectories,
2478
- paneAccounts,
2479
- openSmartPane,
2480
- normalizeRemote,
2481
- normalizeGateQuestions,
2482
- normalizeGateOptions,
2483
- listTeams,
2484
- listSettings,
2485
- listRuns,
2486
- isMigrated,
2487
- identityFromRemote,
2488
- herdWrapUp,
2489
- herdStopHidden,
2490
- herdStatus,
2491
- herdStart,
2492
- herdSpawn,
2493
- herdResume,
2494
- herdReport,
2495
- herdMilestone,
2496
- herdList,
2497
- herdGates,
2498
- herdClose,
2499
- herdAttend,
2500
- herdAsk,
2501
- herdAnswer,
2502
- guardTestDaemonEnv,
2503
- getSetting,
2504
- getRun,
2505
- getDef,
2506
- gateWait,
2507
- gateUnsubscribe,
2508
- gateSubscriptions,
2509
- gateSubscribe,
2510
- gatePresentation,
2511
- gatePark,
2512
- gateOptionValue,
2513
- gateOptionLabel,
2514
- gateOpen,
2515
- gateList,
2516
- gateClose,
2517
- gateAsk,
2518
- gateAnswer,
2519
- formatPaneRef,
2520
- explainSetting,
2521
- expandVariables,
2522
- eventsWait,
2523
- eventsList,
2524
- eventsHead,
2525
- eventsEmit,
2526
- deriveRepoIdentity,
2527
- decidePlacement,
2528
- daemonHealth,
2529
- createRelay,
2530
- clearIdentityMemo,
2531
- chatWho,
2532
- chatSignOut,
2533
- chatSignIn,
2534
- chatRooms,
2535
- chatRelease,
2536
- chatRead,
2537
- chatPost,
2538
- chatMessages,
2539
- chatMark,
2540
- chatLeave,
2541
- chatJoin,
2542
- chatInvite,
2543
- chatDmOpen,
2544
- chatDm,
2545
- chatClaim,
2546
- chatBuddies,
2547
- chatBack,
2548
- chatAway,
2549
- chatArchive,
2550
- chatAck,
2551
- bgStop,
2552
- bgStatus,
2553
- bgRelease,
2554
- bgEnsure,
2555
- allDefs,
2556
- agentStart,
2557
- agentResume,
2558
- agentList,
2559
- agentGet,
2560
- abandonRun,
2561
- SCOPE_ORDER,
2562
- REGISTRY,
2563
- GATE_FORM_OPTION_CAP,
2564
- GATE_BY_PANE,
2565
- DEFAULT_WS_URL,
2566
- DEFAULT_SOCK,
2547
+ BG_PREFIX,
2567
2548
  COMMAND_NAMES,
2568
- BG_PREFIX
2549
+ DEFAULT_SOCK,
2550
+ DEFAULT_WS_URL,
2551
+ GATE_BY_PANE,
2552
+ GATE_FORM_OPTION_CAP,
2553
+ REGISTRY,
2554
+ SCOPE_ORDER,
2555
+ abandonRun,
2556
+ agentGet,
2557
+ agentList,
2558
+ agentResume,
2559
+ agentStart,
2560
+ allDefs,
2561
+ bgEnsure,
2562
+ bgRelease,
2563
+ bgStatus,
2564
+ bgStop,
2565
+ chatAck,
2566
+ chatArchive,
2567
+ chatAway,
2568
+ chatBack,
2569
+ chatBuddies,
2570
+ chatClaim,
2571
+ chatDm,
2572
+ chatDmOpen,
2573
+ chatInvite,
2574
+ chatJoin,
2575
+ chatLeave,
2576
+ chatMark,
2577
+ chatMessages,
2578
+ chatPost,
2579
+ chatRead,
2580
+ chatRelease,
2581
+ chatRooms,
2582
+ chatSignIn,
2583
+ chatSignOut,
2584
+ chatWho,
2585
+ clearIdentityMemo,
2586
+ createRelay,
2587
+ daemonHealth,
2588
+ decidePlacement,
2589
+ deriveRepoIdentity,
2590
+ eventsEmit,
2591
+ eventsHead,
2592
+ eventsList,
2593
+ eventsWait,
2594
+ expandVariables,
2595
+ explainSetting,
2596
+ formatPaneRef,
2597
+ gateAnswer,
2598
+ gateAsk,
2599
+ gateClose,
2600
+ gateList,
2601
+ gateOpen,
2602
+ gateOptionLabel,
2603
+ gateOptionValue,
2604
+ gatePark,
2605
+ gatePresentation,
2606
+ gateSubscribe,
2607
+ gateSubscriptions,
2608
+ gateUnsubscribe,
2609
+ gateWait,
2610
+ getDef,
2611
+ getRun,
2612
+ getSetting,
2613
+ guardTestDaemonEnv,
2614
+ herdAnswer,
2615
+ herdAsk,
2616
+ herdAttend,
2617
+ herdClose,
2618
+ herdGates,
2619
+ herdList,
2620
+ herdMilestone,
2621
+ herdReport,
2622
+ herdResume,
2623
+ herdSpawn,
2624
+ herdStart,
2625
+ herdStatus,
2626
+ herdStopHidden,
2627
+ herdWrapUp,
2628
+ identityFromRemote,
2629
+ isMigrated,
2630
+ listRuns,
2631
+ listSettings,
2632
+ listTeams,
2633
+ normalizeGateOptions,
2634
+ normalizeGateQuestions,
2635
+ normalizeRemote,
2636
+ openSmartPane,
2637
+ paneAccounts,
2638
+ paneDirectories,
2639
+ paneFocus,
2640
+ paneList,
2641
+ panePeek,
2642
+ paneSend,
2643
+ paneSpawn,
2644
+ parseIdentity,
2645
+ parsePaneRef,
2646
+ readBranchCache,
2647
+ readDiscussions,
2648
+ readMrsByBranch,
2649
+ readProjectMRs,
2650
+ readStore,
2651
+ reconcilerClear,
2652
+ reconcilerStatus,
2653
+ repoNameForPath,
2654
+ resolveForgeToken,
2655
+ resolveNameToIdentity,
2656
+ rtCommand,
2657
+ serializeIdentity,
2658
+ setSetting,
2659
+ setSettingsWarnSink,
2660
+ subscribe,
2661
+ unsetSetting,
2662
+ unwrapGateAnswerValue,
2663
+ validateGateAnswers,
2664
+ validateValue
2569
2665
  };
@@ -47,7 +47,7 @@ function normalizeRemote(remote) {
47
47
  return `${host.toLowerCase()}/${normalizedPath}`;
48
48
  }
49
49
  export {
50
- serializeIdentity,
50
+ normalizeRemote,
51
51
  parseIdentity,
52
- normalizeRemote
52
+ serializeIdentity
53
53
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mattstack/rt-client",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/client.ts CHANGED
@@ -561,6 +561,7 @@ export function herdStart(
561
561
  ): Promise<RtResponse<Commands["herd:start"]["data"]>> {
562
562
  const payload: Record<string, unknown> = { name: a.name, repo: a.repo, session: a.session };
563
563
  if (a.hidden !== undefined) payload.hidden = a.hidden;
564
+ if (a.callerPane !== undefined) payload.callerPane = a.callerPane;
564
565
  return rtCommand<Commands["herd:start"]["data"]>("herd:start", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 60_000 });
565
566
  }
566
567
 
@@ -635,7 +636,9 @@ export function herdResume(
635
636
  a: Commands["herd:resume"]["payload"],
636
637
  o: RtClientOptions = {},
637
638
  ): Promise<RtResponse<Commands["herd:resume"]["data"]>> {
638
- return rtCommand<Commands["herd:resume"]["data"]>("herd:resume", { herd: a.herd, session: a.session }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
639
+ const payload: Record<string, unknown> = { herd: a.herd, session: a.session };
640
+ if (a.callerPane !== undefined) payload.callerPane = a.callerPane;
641
+ return rtCommand<Commands["herd:resume"]["data"]>("herd:resume", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
639
642
  }
640
643
 
641
644
  /** Closes a herdr pane, one CLI call under the runner's own 15s budget. */
package/src/commands.ts CHANGED
@@ -31,12 +31,27 @@ export interface ProjectMRsScope {
31
31
  knownSections?: string[];
32
32
  }
33
33
 
34
+ /** Classified cause of a repo's failing project sync (lib/daemon/project-sync-health.ts). */
35
+ export type ProjectSyncErrorKind = "rate-limited" | "auth" | "server-error" | "timeout" | "other";
36
+
37
+ /** A repo's current unbroken run of failed project syncs. */
38
+ export interface ProjectSyncError {
39
+ /** First failure of the run; later failures leave it alone. */
40
+ since: number;
41
+ lastAt: number;
42
+ kind: ProjectSyncErrorKind;
43
+ /** Raw error text, capped at 200 chars. */
44
+ message: string;
45
+ }
46
+
34
47
  export interface ProjectMRsData {
35
48
  mrs: Record<string, { pr: PullRequest; fetchedAt: number; codeownerSections?: string[] }>;
36
49
  listSyncedAt: number;
37
50
  source: "poll" | "events" | "mutation";
38
51
  syncedAt: number;
39
52
  scope?: ProjectMRsScope;
53
+ /** Present while this repo's most recent project sync failed. */
54
+ syncError?: ProjectSyncError;
40
55
  }
41
56
 
42
57
  export interface DiscussionsData {
@@ -171,7 +186,7 @@ export interface HerdJobInfo { herd: string; name: string; worktree: string; bra
171
186
  /** `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. */
172
187
  export interface HerdStatusData {
173
188
  herd: HerdInfo;
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 }>;
189
+ 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; /** The daemon watchdog's escalation ladder for this job: strikes so far and when it last acted. Absent when the job is not on the ladder (healthy, no watchdog wired, or the daemon restarted since). */ watchdog?: { strikes: number; lastPokeAt: number | null } }>;
175
190
  unread: number;
176
191
  lifecycleConnected: boolean;
177
192
  hiddenUp: boolean | null;
@@ -349,6 +364,11 @@ export interface ReconcilerStatus {
349
364
  executors: ExecutorView[];
350
365
  }
351
366
 
367
+ /** What a claude spawn's folder-trust check established, when one ran:
368
+ `none` = no dialog was up, `accepted` = one was answered, `stuck` = one is
369
+ still up and needs a human, `unchecked` = nothing could be established. */
370
+ export type TrustOutcome = "none" | "accepted" | "stuck" | "unchecked";
371
+
352
372
  export interface AgentRecord {
353
373
  id: string; repo: string; cwd: string; provider: string;
354
374
  surface: AgentSurface; sessionId: string;
@@ -459,8 +479,14 @@ export interface WorktreeProvisionData {
459
479
  tree: string; path: string; branch: string; wasOnDeck: boolean;
460
480
  readyAt: string | null; branchState: "new" | "tracking-remote" | "existing-clean" | "diverged" | "behind";
461
481
  readyFailed?: true; failedStep?: string;
482
+ /** Set only when the tree was built by hydrating from the repo's golden donor, not the on-deck pool or a cold create. */
483
+ hydratedFrom?: string;
484
+ }
485
+ export interface WorktreeCreateData {
486
+ tree: string; path: string;
487
+ /** Set only for `--on-deck`, and only when that tree hydrated from the golden rather than cold-creating. */
488
+ hydratedFrom?: string;
462
489
  }
463
- export interface WorktreeCreateData { tree: string; path: string }
464
490
  export interface WorktreeDisposeData {
465
491
  disposed: string[];
466
492
  /** `detail` is set only for a refusal whose bare `reason` code can't name
@@ -500,6 +526,30 @@ export type MRActionName =
500
526
  | "retryJob" | "retryPipeline"
501
527
  | "toggleDraft" | "requestReReview";
502
528
 
529
+ /** One worktree's git badge as the daemon sweep computed it. All timestamps ISO 8601. */
530
+ export interface GitWorktreeBadge {
531
+ worktree: string;
532
+ branch: string | null;
533
+ detached: boolean;
534
+ staged: number;
535
+ unstaged: number;
536
+ untracked: number;
537
+ conflicted: number;
538
+ clean: boolean;
539
+ ahead: number | null;
540
+ behind: number | null;
541
+ upstream: string | null;
542
+ lastFetchedAt: string | null;
543
+ updatedAt: string;
544
+ }
545
+
546
+ /** repo is the serialized identity (the repo-index key). error is set when the last sweep could not read the repo; stale worktrees may accompany it. */
547
+ export interface RepoStatusRow {
548
+ repo: string;
549
+ worktrees: GitWorktreeBadge[];
550
+ error: string | null;
551
+ }
552
+
503
553
  export interface Commands {
504
554
  "project-mrs:read": { payload: { repoName: string; maxAgeMs?: number; demand?: DemandDecl }; data: ProjectMRsData };
505
555
  "discussions:read": { payload: { repoName: string; iid: number }; data: DiscussionsData };
@@ -618,7 +668,7 @@ export interface Commands {
618
668
  "chat:dm-open": { payload: { from: string; to: string; sessionId?: string }; data: { room: string; created: boolean } };
619
669
 
620
670
  // ─── Agent handoff (rt agent) ────────────────────────────────────────────
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 };
671
+ "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; /** How long the folder-trust check lets the dialog paint before reading the screen; interactive launches keep the short default, herd:spawn passes its own longer budget. */ trustWaitMs?: number }; data: AgentRecord & { trust?: TrustOutcome } };
622
672
  "agent:resume": { payload: { id: string; prompt?: string; surface?: AgentSurface; workspace?: string; tab?: string }; data: AgentRecord };
623
673
  "agent:get": { payload: { id: string }; data: AgentRecord };
624
674
  "agent:list": { payload: { repo?: string }; data: { agents: AgentRecord[] } };
@@ -679,6 +729,7 @@ export interface Commands {
679
729
  "endpoint:status": { payload: { repo?: string }; data: EndpointStatusData };
680
730
 
681
731
  "repos:locate": { payload: { newPath: string; repo?: string; dryRun?: boolean }; data: unknown };
732
+ "repos:status": { payload: { refresh?: boolean }; data: { repos: RepoStatusRow[]; sweptAt: string | null } };
682
733
  "freshness:reconcile": { payload: Record<string, never>; data: unknown };
683
734
 
684
735
  // ─── Reconciler (executor state; lib/daemon/reconciler.ts) ───────────────
@@ -716,8 +767,17 @@ export interface Commands {
716
767
  /** `contextOmitted` appears only when the gate context plus every
717
768
  question's `context` exceeded their shared 8192-byte budget: the
718
769
  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 };
770
+ context too when it was over the budget on its own.
771
+ `formCapExceeded`/`formCapAdvisory` appear only when the pane could
772
+ have presented an in-pane form and one or more questions exceeded
773
+ its 4-option cap, which is the one thing that forced this gate to
774
+ `wait`: the gate still opens, and the advisory names the structural
775
+ fix so the caller can re-author. */
776
+ data: {
777
+ id: string; presentation: "form" | "wait"; subject: string; supersededId: string | null; contextOmitted?: true;
778
+ formCapExceeded?: Array<{ question: string; options: number }>;
779
+ formCapAdvisory?: string;
780
+ };
721
781
  };
722
782
  /**
723
783
  * A CAS loss is a DEFINED OUTCOME, not an error: `ok:true` with
@@ -751,8 +811,8 @@ export interface Commands {
751
811
  "gate:subscriptions": { payload: { session?: string; live?: boolean }; data: { subscriptions: GateSubscription[] } };
752
812
 
753
813
  // ─── Herd (shepherd run registry) ────────────────────────────────────────
754
- "herd:start": { payload: { name: string; repo: string; session: string; hidden?: boolean }; data: { herd: string; room: string; workspace: string; subscription: string; handle: string; hidden: boolean } };
755
- "herd:resume": { payload: { herd: string; session: string }; data: { subscription: string; gates: GateRow[]; unread: number; status: HerdStatusData; handle: string } };
814
+ "herd:start": { payload: { name: string; repo: string; session: string; hidden?: boolean; callerPane?: string }; data: { herd: string; room: string; workspace: string; subscription: string; handle: string; hidden: boolean } };
815
+ "herd:resume": { payload: { herd: string; session: string; callerPane?: string }; data: { subscription: string; gates: GateRow[]; unread: number; status: HerdStatusData; handle: string } };
756
816
  "herd:status": { payload: { herd: string }; data: HerdStatusData };
757
817
  /** Active herds only unless `all`, so a shepherd's "which herd am I on" question has one answer. */
758
818
  "herd:list": { payload: { all?: boolean }; data: { herds: HerdListRow[] } };
@@ -870,6 +930,7 @@ export const COMMAND_NAMES: readonly CommandName[] = [
870
930
  "endpoint:release",
871
931
  "endpoint:status",
872
932
  "repos:locate",
933
+ "repos:status",
873
934
  "freshness:reconcile",
874
935
  "reconciler:status",
875
936
  "reconciler:clear",
package/src/index.ts CHANGED
@@ -88,6 +88,8 @@ export type {
88
88
  Discussion,
89
89
  DemandDecl,
90
90
  ProjectMRsScope,
91
+ ProjectSyncError,
92
+ ProjectSyncErrorKind,
91
93
  ProjectMRsData,
92
94
  DiscussionsData,
93
95
  MrByBranchEntry,
@@ -222,6 +222,16 @@ export const REGISTRY: readonly SettingDef[] = [
222
222
  migrated: true,
223
223
  description: "Age floor in days for the log janitor pruning every surface's rotated log files under ~/.mattstack/rt/logs (default 14). A fresh key, not an ownership-latch port, so a default is fine here.",
224
224
  },
225
+ {
226
+ key: "rt.gitStatus",
227
+ type: "object",
228
+ scopes: ALL_SCOPES,
229
+ default: { sweep: true, sweepIntervalSec: 300, fetchIntervalSec: 900 },
230
+ merge: "deep",
231
+ repoScoped: true,
232
+ migrated: true,
233
+ description: "Mission-control git badge sweep. sweep gates the daemon sweep and fetchIntervalSec (the background fetch cadence, 0 disables fetching) are both read per repo, so a per-repo override of either takes effect (a repo override of { sweep: false } opts that repo out). sweepIntervalSec, the minimum seconds between sweeps, is read only from the global config by the sweep tick; a per-repo override of sweepIntervalSec has no effect.",
234
+ },
225
235
  {
226
236
  key: "rt.logLevel",
227
237
  type: "string",
@@ -751,4 +761,86 @@ export const REGISTRY: readonly SettingDef[] = [
751
761
  merge: "replace",
752
762
  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.",
753
763
  },
764
+
765
+ // --- herd (watchdog) ---------------------------------------------------
766
+ {
767
+ key: "herd.watchdog.enabled",
768
+ type: "boolean",
769
+ scopes: ["machine"],
770
+ default: true,
771
+ merge: "replace",
772
+ description: "Turns the herd watchdog sweep on.",
773
+ },
774
+ {
775
+ key: "herd.watchdog.fastMins",
776
+ type: "number",
777
+ scopes: ["machine"],
778
+ default: 2,
779
+ merge: "replace",
780
+ description: "Idle minutes before a worker with a pending DM, mention, or answered gate is poked.",
781
+ },
782
+ {
783
+ key: "herd.watchdog.shepherdFastMins",
784
+ type: "number",
785
+ scopes: ["machine"],
786
+ default: 5,
787
+ merge: "replace",
788
+ description: "Minutes an open human-owned gate in the herd may stay unanswered before the shepherd is poked. Unread DMs and mentions for the shepherd wedge immediately and are not delayed by this key.",
789
+ },
790
+ {
791
+ key: "herd.watchdog.backstopMins",
792
+ type: "number",
793
+ scopes: ["machine"],
794
+ default: 15,
795
+ merge: "replace",
796
+ description: "Idle minutes before any non-gated active worker is poked.",
797
+ },
798
+ {
799
+ key: "herd.watchdog.retryMins",
800
+ type: "number",
801
+ scopes: ["machine"],
802
+ default: 5,
803
+ merge: "replace",
804
+ description: "Minutes between escalation strikes while the watchdog keeps poking an unresponsive worker.",
805
+ },
806
+ {
807
+ key: "herd.watchdog.notifyQuietMins",
808
+ type: "number",
809
+ scopes: ["machine"],
810
+ default: 30,
811
+ merge: "replace",
812
+ description: "Minimum minutes between macOS notifications for the same herd.",
813
+ },
814
+ {
815
+ key: "herd.watchdog.nagMins",
816
+ type: "number",
817
+ scopes: ["machine"],
818
+ default: 30,
819
+ merge: "replace",
820
+ description: "Minutes a done job may hold its pane before the shepherd is nagged to reclaim it.",
821
+ },
822
+ {
823
+ key: "herd.watchdog.notifyHuman",
824
+ type: "boolean",
825
+ scopes: ["machine"],
826
+ default: true,
827
+ merge: "replace",
828
+ description: "Whether the watchdog's top escalation rung sends a macOS notification to the human.",
829
+ },
830
+ {
831
+ key: "herd.watchdog.midRunTrustAccept",
832
+ type: "boolean",
833
+ scopes: ["machine"],
834
+ default: false,
835
+ merge: "replace",
836
+ description: "Whether the watchdog may drive a mid-run folder-trust dialog on a daemon-provisioned tree itself, rather than only parking the job and notifying (RT-196). Off by default: the screen match cannot yet confirm the dialog's folder matches the job's worktree, so a working session showing an unrelated permission prompt with the same shape is a real risk.",
837
+ },
838
+ {
839
+ key: "panes.relocationAutoAccept",
840
+ type: "boolean",
841
+ scopes: ["machine"],
842
+ default: true,
843
+ merge: "replace",
844
+ description: "Whether the daemon may answer Claude Code's EnterWorktree permission-root relocation prompt on a blocked pane by itself (RT-200). Unlike the trust dialog, the prompt names the worktree path in its own body, and the daemon accepts only when that exact path is in rt's worktree registry, so this is on by default. Read by both the herd watchdog and the executor reconciler.",
845
+ },
754
846
  ];