@mattstack/rt-client 0.28.0 → 0.30.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[];
@@ -140,6 +153,7 @@ export interface GateAnswer {
140
153
  answers: Record<string, string | string[] | {
141
154
  value: string | string[];
142
155
  note?: string;
156
+ text?: string;
143
157
  }>;
144
158
  by: string;
145
159
  answeredAt: number;
@@ -691,10 +705,14 @@ export interface WorktreeProvisionData {
691
705
  branchState: "new" | "tracking-remote" | "existing-clean" | "diverged" | "behind";
692
706
  readyFailed?: true;
693
707
  failedStep?: string;
708
+ /** Set only when the tree was built by hydrating from the repo's golden donor, not the on-deck pool or a cold create. */
709
+ hydratedFrom?: string;
694
710
  }
695
711
  export interface WorktreeCreateData {
696
712
  tree: string;
697
713
  path: string;
714
+ /** Set only for `--on-deck`, and only when that tree hydrated from the golden rather than cold-creating. */
715
+ hydratedFrom?: string;
698
716
  }
699
717
  export interface WorktreeDisposeData {
700
718
  disposed: string[];
@@ -787,6 +805,28 @@ export interface DiscussionsDiffsData {
787
805
  truncated: boolean;
788
806
  }
789
807
  export type MRActionName = "merge" | "rebase" | "approve" | "unapprove" | "setAutoMerge" | "cancelAutoMerge" | "retryJob" | "retryPipeline" | "toggleDraft" | "requestReReview";
808
+ /** One worktree's git badge as the daemon sweep computed it. All timestamps ISO 8601. */
809
+ export interface GitWorktreeBadge {
810
+ worktree: string;
811
+ branch: string | null;
812
+ detached: boolean;
813
+ staged: number;
814
+ unstaged: number;
815
+ untracked: number;
816
+ conflicted: number;
817
+ clean: boolean;
818
+ ahead: number | null;
819
+ behind: number | null;
820
+ upstream: string | null;
821
+ lastFetchedAt: string | null;
822
+ updatedAt: string;
823
+ }
824
+ /** 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. */
825
+ export interface RepoStatusRow {
826
+ repo: string;
827
+ worktrees: GitWorktreeBadge[];
828
+ error: string | null;
829
+ }
790
830
  export interface Commands {
791
831
  "project-mrs:read": {
792
832
  payload: {
@@ -1456,6 +1496,15 @@ export interface Commands {
1456
1496
  };
1457
1497
  data: unknown;
1458
1498
  };
1499
+ "repos:status": {
1500
+ payload: {
1501
+ refresh?: boolean;
1502
+ };
1503
+ data: {
1504
+ repos: RepoStatusRow[];
1505
+ sweptAt: string | null;
1506
+ };
1507
+ };
1459
1508
  "freshness:reconcile": {
1460
1509
  payload: Record<string, never>;
1461
1510
  data: unknown;
@@ -1524,13 +1573,23 @@ export interface Commands {
1524
1573
  /** `contextOmitted` appears only when the gate context plus every
1525
1574
  question's `context` exceeded their shared 8192-byte budget: the
1526
1575
  gate still opened, but question contexts were dropped, and the gate
1527
- context too when it was over the budget on its own. */
1576
+ context too when it was over the budget on its own.
1577
+ `formCapExceeded`/`formCapAdvisory` appear only when the pane could
1578
+ have presented an in-pane form and one or more questions exceeded
1579
+ its 4-option cap, which is the one thing that forced this gate to
1580
+ `wait`: the gate still opens, and the advisory names the structural
1581
+ fix so the caller can re-author. */
1528
1582
  data: {
1529
1583
  id: string;
1530
1584
  presentation: "form" | "wait";
1531
1585
  subject: string;
1532
1586
  supersededId: string | null;
1533
1587
  contextOmitted?: true;
1588
+ formCapExceeded?: Array<{
1589
+ question: string;
1590
+ options: number;
1591
+ }>;
1592
+ formCapAdvisory?: string;
1534
1593
  };
1535
1594
  };
1536
1595
  /**
@@ -1,7 +1,8 @@
1
1
  import type { GateQuestion, GateAnswer } from "./commands.ts";
2
2
  export type GateAnswerWire = GateAnswer["answers"][string];
3
- /** Both wire shapes carry the same value underneath: bare, or {value, note?}
4
- when a panel attaches free text. Validation reads only the value. */
3
+ /** Both wire shapes carry the same value underneath: bare, or {value,
4
+ note?, text?} when a panel attaches free text or a replacement for text
5
+ the gate offered. Unwrapping keeps only the value. */
5
6
  export declare function unwrapGateAnswerValue(raw: unknown): unknown;
6
7
  /** Option membership is required whenever a question declares options,
7
8
  checked against the unwrapped value (every element, for multi); an
package/dist/gate.js CHANGED
@@ -54,12 +54,18 @@ function unwrapGateAnswerValue(raw) {
54
54
  }
55
55
  return raw;
56
56
  }
57
- function wrapperNoteIsValid(raw) {
57
+ function wrapperFieldError(qid, raw) {
58
58
  if (!raw || typeof raw !== "object" || Array.isArray(raw) || !("value" in raw)) {
59
- return true;
59
+ return null;
60
60
  }
61
- const note = raw.note;
62
- return note === undefined || typeof note === "string";
61
+ const { note, text } = raw;
62
+ if (note !== undefined && typeof note !== "string")
63
+ return `question ${qid} note must be a string`;
64
+ if (text !== undefined && typeof text !== "string")
65
+ return `question ${qid} text must be a string`;
66
+ if (typeof text === "string" && text.trim() === "")
67
+ return `question ${qid} text must not be empty`;
68
+ return null;
63
69
  }
64
70
  function validateGateAnswers(questions, answers) {
65
71
  const byId = new Map(questions.map((q) => [q.id, q]));
@@ -67,8 +73,9 @@ function validateGateAnswers(questions, answers) {
67
73
  const question = byId.get(qid);
68
74
  if (!question)
69
75
  return `unknown question id: ${qid}`;
70
- if (!wrapperNoteIsValid(raw))
71
- return `question ${qid} note must be a string`;
76
+ const wrapperError = wrapperFieldError(qid, raw);
77
+ if (wrapperError)
78
+ return wrapperError;
72
79
  const value = unwrapGateAnswerValue(raw);
73
80
  const isArray = Array.isArray(value);
74
81
  if (question.multi && !isArray)
@@ -99,12 +106,12 @@ function gatePresentation(args) {
99
106
  return args.questions.every((q) => q.options.length <= GATE_FORM_OPTION_CAP) ? "form" : "wait";
100
107
  }
101
108
  export {
102
- validateGateAnswers,
103
- unwrapGateAnswerValue,
104
- normalizeGateQuestions,
105
- normalizeGateOptions,
106
- gatePresentation,
107
- gateOptionValue,
109
+ GATE_FORM_OPTION_CAP,
108
110
  gateOptionLabel,
109
- GATE_FORM_OPTION_CAP
111
+ gateOptionValue,
112
+ gatePresentation,
113
+ normalizeGateOptions,
114
+ normalizeGateQuestions,
115
+ unwrapGateAnswerValue,
116
+ validateGateAnswers
110
117
  };
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
@@ -582,6 +582,7 @@ var COMMAND_NAMES = [
582
582
  "endpoint:release",
583
583
  "endpoint:status",
584
584
  "repos:locate",
585
+ "repos:status",
585
586
  "freshness:reconcile",
586
587
  "reconciler:status",
587
588
  "reconciler:clear",
@@ -642,12 +643,18 @@ function unwrapGateAnswerValue(raw) {
642
643
  }
643
644
  return raw;
644
645
  }
645
- function wrapperNoteIsValid(raw) {
646
+ function wrapperFieldError(qid, raw) {
646
647
  if (!raw || typeof raw !== "object" || Array.isArray(raw) || !("value" in raw)) {
647
- return true;
648
+ return null;
648
649
  }
649
- const note = raw.note;
650
- return note === undefined || typeof note === "string";
650
+ const { note, text } = raw;
651
+ if (note !== undefined && typeof note !== "string")
652
+ return `question ${qid} note must be a string`;
653
+ if (text !== undefined && typeof text !== "string")
654
+ return `question ${qid} text must be a string`;
655
+ if (typeof text === "string" && text.trim() === "")
656
+ return `question ${qid} text must not be empty`;
657
+ return null;
651
658
  }
652
659
  function validateGateAnswers(questions, answers) {
653
660
  const byId = new Map(questions.map((q) => [q.id, q]));
@@ -655,8 +662,9 @@ function validateGateAnswers(questions, answers) {
655
662
  const question = byId.get(qid);
656
663
  if (!question)
657
664
  return `unknown question id: ${qid}`;
658
- if (!wrapperNoteIsValid(raw))
659
- return `question ${qid} note must be a string`;
665
+ const wrapperError = wrapperFieldError(qid, raw);
666
+ if (wrapperError)
667
+ return wrapperError;
660
668
  const value = unwrapGateAnswerValue(raw);
661
669
  const isArray = Array.isArray(value);
662
670
  if (question.multi && !isArray)
@@ -1120,6 +1128,16 @@ var REGISTRY = [
1120
1128
  migrated: true,
1121
1129
  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."
1122
1130
  },
1131
+ {
1132
+ key: "rt.gitStatus",
1133
+ type: "object",
1134
+ scopes: ALL_SCOPES,
1135
+ default: { sweep: true, sweepIntervalSec: 300, fetchIntervalSec: 900 },
1136
+ merge: "deep",
1137
+ repoScoped: true,
1138
+ migrated: true,
1139
+ 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."
1140
+ },
1123
1141
  {
1124
1142
  key: "rt.logLevel",
1125
1143
  type: "string",
@@ -1682,6 +1700,14 @@ var REGISTRY = [
1682
1700
  default: false,
1683
1701
  merge: "replace",
1684
1702
  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."
1703
+ },
1704
+ {
1705
+ key: "panes.relocationAutoAccept",
1706
+ type: "boolean",
1707
+ scopes: ["machine"],
1708
+ default: true,
1709
+ merge: "replace",
1710
+ 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."
1685
1711
  }
1686
1712
  ];
1687
1713
 
@@ -1856,11 +1882,11 @@ function expandString(input, ctx) {
1856
1882
  return match;
1857
1883
  });
1858
1884
  }
1859
- function teamPath(teamsDir2, name) {
1885
+ function teamPath(teamsDir, name) {
1860
1886
  if (name.includes("/") || name.includes("\\") || name.includes("..")) {
1861
1887
  throw new Error(`rt: cannot expand \${team:${name}} — a team name must be a single directory segment (no "/", "\\" or "..")`);
1862
1888
  }
1863
- return join6(teamsDir2, name);
1889
+ return join6(teamsDir, name);
1864
1890
  }
1865
1891
  function required(value, name, needs) {
1866
1892
  if (value === undefined || value === "") {
@@ -2525,122 +2551,122 @@ async function resolveNameToIdentity(name, reposJsonPath) {
2525
2551
  }
2526
2552
  }
2527
2553
  export {
2528
- validateValue,
2529
- validateGateAnswers,
2530
- unwrapGateAnswerValue,
2531
- unsetSetting,
2532
- subscribe,
2533
- setSettingsWarnSink,
2534
- setSetting,
2535
- serializeIdentity,
2536
- rtCommand,
2537
- resolveNameToIdentity,
2538
- resolveForgeToken,
2539
- repoNameForPath,
2540
- reconcilerStatus,
2541
- reconcilerClear,
2542
- readStore,
2543
- readProjectMRs,
2544
- readMrsByBranch,
2545
- readDiscussions,
2546
- readBranchCache,
2547
- parsePaneRef,
2548
- parseIdentity,
2549
- paneSpawn,
2550
- paneSend,
2551
- panePeek,
2552
- paneList,
2553
- paneFocus,
2554
- paneDirectories,
2555
- paneAccounts,
2556
- openSmartPane,
2557
- normalizeRemote,
2558
- normalizeGateQuestions,
2559
- normalizeGateOptions,
2560
- listTeams,
2561
- listSettings,
2562
- listRuns,
2563
- isMigrated,
2564
- identityFromRemote,
2565
- herdWrapUp,
2566
- herdStopHidden,
2567
- herdStatus,
2568
- herdStart,
2569
- herdSpawn,
2570
- herdResume,
2571
- herdReport,
2572
- herdMilestone,
2573
- herdList,
2574
- herdGates,
2575
- herdClose,
2576
- herdAttend,
2577
- herdAsk,
2578
- herdAnswer,
2579
- guardTestDaemonEnv,
2580
- getSetting,
2581
- getRun,
2582
- getDef,
2583
- gateWait,
2584
- gateUnsubscribe,
2585
- gateSubscriptions,
2586
- gateSubscribe,
2587
- gatePresentation,
2588
- gatePark,
2589
- gateOptionValue,
2590
- gateOptionLabel,
2591
- gateOpen,
2592
- gateList,
2593
- gateClose,
2594
- gateAsk,
2595
- gateAnswer,
2596
- formatPaneRef,
2597
- explainSetting,
2598
- expandVariables,
2599
- eventsWait,
2600
- eventsList,
2601
- eventsHead,
2602
- eventsEmit,
2603
- deriveRepoIdentity,
2604
- decidePlacement,
2605
- daemonHealth,
2606
- createRelay,
2607
- clearIdentityMemo,
2608
- chatWho,
2609
- chatSignOut,
2610
- chatSignIn,
2611
- chatRooms,
2612
- chatRelease,
2613
- chatRead,
2614
- chatPost,
2615
- chatMessages,
2616
- chatMark,
2617
- chatLeave,
2618
- chatJoin,
2619
- chatInvite,
2620
- chatDmOpen,
2621
- chatDm,
2622
- chatClaim,
2623
- chatBuddies,
2624
- chatBack,
2625
- chatAway,
2626
- chatArchive,
2627
- chatAck,
2628
- bgStop,
2629
- bgStatus,
2630
- bgRelease,
2631
- bgEnsure,
2632
- allDefs,
2633
- agentStart,
2634
- agentResume,
2635
- agentList,
2636
- agentGet,
2637
- abandonRun,
2638
- SCOPE_ORDER,
2639
- REGISTRY,
2640
- GATE_FORM_OPTION_CAP,
2641
- GATE_BY_PANE,
2642
- DEFAULT_WS_URL,
2643
- DEFAULT_SOCK,
2554
+ BG_PREFIX,
2644
2555
  COMMAND_NAMES,
2645
- BG_PREFIX
2556
+ DEFAULT_SOCK,
2557
+ DEFAULT_WS_URL,
2558
+ GATE_BY_PANE,
2559
+ GATE_FORM_OPTION_CAP,
2560
+ REGISTRY,
2561
+ SCOPE_ORDER,
2562
+ abandonRun,
2563
+ agentGet,
2564
+ agentList,
2565
+ agentResume,
2566
+ agentStart,
2567
+ allDefs,
2568
+ bgEnsure,
2569
+ bgRelease,
2570
+ bgStatus,
2571
+ bgStop,
2572
+ chatAck,
2573
+ chatArchive,
2574
+ chatAway,
2575
+ chatBack,
2576
+ chatBuddies,
2577
+ chatClaim,
2578
+ chatDm,
2579
+ chatDmOpen,
2580
+ chatInvite,
2581
+ chatJoin,
2582
+ chatLeave,
2583
+ chatMark,
2584
+ chatMessages,
2585
+ chatPost,
2586
+ chatRead,
2587
+ chatRelease,
2588
+ chatRooms,
2589
+ chatSignIn,
2590
+ chatSignOut,
2591
+ chatWho,
2592
+ clearIdentityMemo,
2593
+ createRelay,
2594
+ daemonHealth,
2595
+ decidePlacement,
2596
+ deriveRepoIdentity,
2597
+ eventsEmit,
2598
+ eventsHead,
2599
+ eventsList,
2600
+ eventsWait,
2601
+ expandVariables,
2602
+ explainSetting,
2603
+ formatPaneRef,
2604
+ gateAnswer,
2605
+ gateAsk,
2606
+ gateClose,
2607
+ gateList,
2608
+ gateOpen,
2609
+ gateOptionLabel,
2610
+ gateOptionValue,
2611
+ gatePark,
2612
+ gatePresentation,
2613
+ gateSubscribe,
2614
+ gateSubscriptions,
2615
+ gateUnsubscribe,
2616
+ gateWait,
2617
+ getDef,
2618
+ getRun,
2619
+ getSetting,
2620
+ guardTestDaemonEnv,
2621
+ herdAnswer,
2622
+ herdAsk,
2623
+ herdAttend,
2624
+ herdClose,
2625
+ herdGates,
2626
+ herdList,
2627
+ herdMilestone,
2628
+ herdReport,
2629
+ herdResume,
2630
+ herdSpawn,
2631
+ herdStart,
2632
+ herdStatus,
2633
+ herdStopHidden,
2634
+ herdWrapUp,
2635
+ identityFromRemote,
2636
+ isMigrated,
2637
+ listRuns,
2638
+ listSettings,
2639
+ listTeams,
2640
+ normalizeGateOptions,
2641
+ normalizeGateQuestions,
2642
+ normalizeRemote,
2643
+ openSmartPane,
2644
+ paneAccounts,
2645
+ paneDirectories,
2646
+ paneFocus,
2647
+ paneList,
2648
+ panePeek,
2649
+ paneSend,
2650
+ paneSpawn,
2651
+ parseIdentity,
2652
+ parsePaneRef,
2653
+ readBranchCache,
2654
+ readDiscussions,
2655
+ readMrsByBranch,
2656
+ readProjectMRs,
2657
+ readStore,
2658
+ reconcilerClear,
2659
+ reconcilerStatus,
2660
+ repoNameForPath,
2661
+ resolveForgeToken,
2662
+ resolveNameToIdentity,
2663
+ rtCommand,
2664
+ serializeIdentity,
2665
+ setSetting,
2666
+ setSettingsWarnSink,
2667
+ subscribe,
2668
+ unsetSetting,
2669
+ unwrapGateAnswerValue,
2670
+ validateGateAnswers,
2671
+ validateValue
2646
2672
  };
@@ -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.28.0",
3
+ "version": "0.30.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
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 {
@@ -118,7 +133,7 @@ export { gateOptionValue, gateOptionLabel } from "./gate-options.ts";
118
133
  doorbell it would otherwise send back to the writer. Optional: a caller
119
134
  that supplies none (the board status-bin answers `by: "pane"` with no
120
135
  session) still matches self by `by === GATE_BY_PANE`. */
121
- export interface GateAnswer { answers: Record<string, string | string[] | { value: string | string[]; note?: string }>; by: string; answeredAt: number; overridden?: boolean; session?: string }
136
+ export interface GateAnswer { answers: Record<string, string | string[] | { value: string | string[]; note?: string; text?: string }>; by: string; answeredAt: number; overridden?: boolean; session?: string }
122
137
  export interface GateRow {
123
138
  id: string; subject: string; kind: string;
124
139
  questions: GateQuestion[]; meta: Record<string, unknown> | null;
@@ -464,8 +479,14 @@ export interface WorktreeProvisionData {
464
479
  tree: string; path: string; branch: string; wasOnDeck: boolean;
465
480
  readyAt: string | null; branchState: "new" | "tracking-remote" | "existing-clean" | "diverged" | "behind";
466
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;
467
489
  }
468
- export interface WorktreeCreateData { tree: string; path: string }
469
490
  export interface WorktreeDisposeData {
470
491
  disposed: string[];
471
492
  /** `detail` is set only for a refusal whose bare `reason` code can't name
@@ -505,6 +526,30 @@ export type MRActionName =
505
526
  | "retryJob" | "retryPipeline"
506
527
  | "toggleDraft" | "requestReReview";
507
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
+
508
553
  export interface Commands {
509
554
  "project-mrs:read": { payload: { repoName: string; maxAgeMs?: number; demand?: DemandDecl }; data: ProjectMRsData };
510
555
  "discussions:read": { payload: { repoName: string; iid: number }; data: DiscussionsData };
@@ -684,6 +729,7 @@ export interface Commands {
684
729
  "endpoint:status": { payload: { repo?: string }; data: EndpointStatusData };
685
730
 
686
731
  "repos:locate": { payload: { newPath: string; repo?: string; dryRun?: boolean }; data: unknown };
732
+ "repos:status": { payload: { refresh?: boolean }; data: { repos: RepoStatusRow[]; sweptAt: string | null } };
687
733
  "freshness:reconcile": { payload: Record<string, never>; data: unknown };
688
734
 
689
735
  // ─── Reconciler (executor state; lib/daemon/reconciler.ts) ───────────────
@@ -721,8 +767,17 @@ export interface Commands {
721
767
  /** `contextOmitted` appears only when the gate context plus every
722
768
  question's `context` exceeded their shared 8192-byte budget: the
723
769
  gate still opened, but question contexts were dropped, and the gate
724
- context too when it was over the budget on its own. */
725
- 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
+ };
726
781
  };
727
782
  /**
728
783
  * A CAS loss is a DEFINED OUTCOME, not an error: `ok:true` with
@@ -875,6 +930,7 @@ export const COMMAND_NAMES: readonly CommandName[] = [
875
930
  "endpoint:release",
876
931
  "endpoint:status",
877
932
  "repos:locate",
933
+ "repos:status",
878
934
  "freshness:reconcile",
879
935
  "reconciler:status",
880
936
  "reconciler:clear",
@@ -3,8 +3,9 @@ import { gateOptionValue } from "./gate-options.ts";
3
3
 
4
4
  export type GateAnswerWire = GateAnswer["answers"][string];
5
5
 
6
- /** Both wire shapes carry the same value underneath: bare, or {value, note?}
7
- when a panel attaches free text. Validation reads only the value. */
6
+ /** Both wire shapes carry the same value underneath: bare, or {value,
7
+ note?, text?} when a panel attaches free text or a replacement for text
8
+ the gate offered. Unwrapping keeps only the value. */
8
9
  export function unwrapGateAnswerValue(raw: unknown): unknown {
9
10
  if (raw && typeof raw === "object" && !Array.isArray(raw) && "value" in (raw as Record<string, unknown>)) {
10
11
  return (raw as { value: unknown }).value;
@@ -12,12 +13,15 @@ export function unwrapGateAnswerValue(raw: unknown): unknown {
12
13
  return raw;
13
14
  }
14
15
 
15
- function wrapperNoteIsValid(raw: unknown): boolean {
16
+ function wrapperFieldError(qid: string, raw: unknown): string | null {
16
17
  if (!raw || typeof raw !== "object" || Array.isArray(raw) || !("value" in (raw as Record<string, unknown>))) {
17
- return true;
18
+ return null;
18
19
  }
19
- const note = (raw as Record<string, unknown>).note;
20
- return note === undefined || typeof note === "string";
20
+ const { note, text } = raw as Record<string, unknown>;
21
+ if (note !== undefined && typeof note !== "string") return `question ${qid} note must be a string`;
22
+ if (text !== undefined && typeof text !== "string") return `question ${qid} text must be a string`;
23
+ if (typeof text === "string" && text.trim() === "") return `question ${qid} text must not be empty`;
24
+ return null;
21
25
  }
22
26
 
23
27
  /** Option membership is required whenever a question declares options,
@@ -32,7 +36,8 @@ export function validateGateAnswers(
32
36
  for (const [qid, raw] of Object.entries(answers)) {
33
37
  const question = byId.get(qid);
34
38
  if (!question) return `unknown question id: ${qid}`;
35
- if (!wrapperNoteIsValid(raw)) return `question ${qid} note must be a string`;
39
+ const wrapperError = wrapperFieldError(qid, raw);
40
+ if (wrapperError) return wrapperError;
36
41
  const value = unwrapGateAnswerValue(raw);
37
42
  const isArray = Array.isArray(value);
38
43
  if (question.multi && !isArray) return `question ${qid} expects an array (multi)`;
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",
@@ -825,4 +835,12 @@ export const REGISTRY: readonly SettingDef[] = [
825
835
  merge: "replace",
826
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.",
827
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
+ },
828
846
  ];