@tritard/waterbrother 0.16.42 → 0.16.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -204,6 +204,7 @@ waterbrother "Search the web for the latest model provider docs about tool calli
204
204
  /runtime-profiles
205
205
  /model anthropic/claude-sonnet-4-20250514
206
206
  /models
207
+ /models all
207
208
 
208
209
  # config
209
210
  waterbrother config set provider anthropic --scope project
@@ -296,7 +297,7 @@ Current Telegram behavior:
296
297
  - pending pairings are explicit and expire automatically after 12 hours unless approved
297
298
  - paired Telegram users drive the same live session and permissions as the terminal operator when the TUI bridge is attached
298
299
  - Telegram now supports remote workspace control with `/cwd`, `/use <path>`, `/desktop`, and `/new-project <name>`
299
- - shared projects now support `/room`, `/members`, `/invites`, `/tasks`, `/mode`, `/claim`, `/release`, `/invite`, `/accept-invite`, `/approve-invite`, `/reject-invite`, `/remove-member`, `/room-runtime`, and `/task ...` from Telegram
300
+ - shared projects now support `/room`, `/events`, `/members`, `/invites`, `/whoami`, `/people`, `/tasks`, `/mode`, `/claim`, `/release`, `/invite`, `/accept-invite`, `/approve-invite`, `/reject-invite`, `/remove-member`, `/room-runtime`, and `/task ...` from Telegram
300
301
  - Telegram now supports `/whoami` for Telegram-id and shared-room membership visibility, plus `/events` for recent shared-room activity
301
302
  - `/room` now includes pending invite count plus task ownership summaries
302
303
  - local `/status` now includes `sharedRoom` with pending invites, task ownership summary, and recent shared-room event activity
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.42",
3
+ "version": "0.16.44",
4
4
  "description": "Waterbrother: bring-your-own-model coding CLI with local tools, sessions, operator modes, and approval controls",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -209,8 +209,8 @@ const INTERACTIVE_COMMANDS = [
209
209
  { name: "/provider", description: "Show active provider" },
210
210
  { name: "/provider <id>", description: "Switch provider for this session" },
211
211
  { name: "/providers", description: "Select provider from list" },
212
- { name: "/model", description: "Show current model" },
213
- { name: "/model <id>", description: "Switch model for this session" },
212
+ { name: "/model", description: "Show current model for this session" },
213
+ { name: "/model <id>", description: "Switch model directly for this session" },
214
214
  { name: "/read <url>", description: "Read and summarize a public URL" },
215
215
  { name: "/search <query>", description: "Search the web and summarize results" },
216
216
  { name: "/open <url|index>", description: "Open a URL or last search result in browser" },
@@ -220,7 +220,8 @@ const INTERACTIVE_COMMANDS = [
220
220
  { name: "/update", description: "Pull latest code, install deps, and run checks" },
221
221
  { name: "/onboarding", description: "Print onboarding/API key guide" },
222
222
  { name: "/onboarding <telegram|discord|signal>", description: "Print messaging service onboarding" },
223
- { name: "/models", description: "Select model from list" },
223
+ { name: "/models", description: "Select model from active provider" },
224
+ { name: "/models all", description: "Browse all known models across providers" },
224
225
  { name: "/feedback", description: "Report a bug or share feedback" },
225
226
  { name: "/cost", description: "Show session token usage and cost breakdown" },
226
227
  { name: "/diff", description: "Show git changes in the current repo" },
@@ -9854,6 +9855,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
9854
9855
  if (line === "/models") {
9855
9856
  try {
9856
9857
  const providerSpec = getProviderSpec(context.runtime.provider);
9858
+ console.log(dim(`Browsing models for active provider: ${context.runtime.provider}`));
9857
9859
  const selectedModel = await chooseRuntimeModelInteractive(agent.getModel(), context.runtime.provider, {
9858
9860
  apiKey: context.runtime.apiKey,
9859
9861
  baseUrl: context.runtime.baseUrl,
@@ -9868,12 +9870,34 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
9868
9870
  model: selectedModel
9869
9871
  });
9870
9872
  console.log(`model set to ${nextRuntime.model}`);
9873
+ console.log(dim('Use /models all to browse the cross-provider catalog.'));
9871
9874
  } catch (error) {
9872
9875
  console.log(`model selection canceled: ${error instanceof Error ? error.message : String(error)}`);
9873
9876
  }
9874
9877
  continue;
9875
9878
  }
9876
9879
 
9880
+ if (line === "/models all") {
9881
+ try {
9882
+ const selectedModel = await chooseAllModelsInteractive(agent.getModel());
9883
+ const { config } = await loadConfigLayers(context.cwd);
9884
+ const nextRuntime = await applyRuntimeSelection({
9885
+ config,
9886
+ context,
9887
+ agent,
9888
+ session: currentSession,
9889
+ model: selectedModel
9890
+ });
9891
+ const keyStatus = runtimeRequiresApiKey(nextRuntime) && (!nextRuntime.apiKey || isLikelyPlaceholderApiKey(nextRuntime.apiKey))
9892
+ ? ' (API key missing)'
9893
+ : '';
9894
+ console.log(`model set to ${nextRuntime.model} (${nextRuntime.provider})${keyStatus}`);
9895
+ } catch (error) {
9896
+ console.log(`model catalog selection canceled: ${error instanceof Error ? error.message : String(error)}`);
9897
+ }
9898
+ continue;
9899
+ }
9900
+
9877
9901
  if (line === "/providers") {
9878
9902
  try {
9879
9903
  const selectedProvider = await chooseProviderInteractive(context.runtime.provider);
package/src/gateway.js CHANGED
@@ -28,6 +28,7 @@ const TELEGRAM_COMMANDS = [
28
28
  { command: "run", description: "Execute an explicit remote prompt" },
29
29
  { command: "status", description: "Show the linked remote session" },
30
30
  { command: "whoami", description: "Show your Telegram identity and room membership" },
31
+ { command: "people", description: "Show recently seen Telegram people in this chat" },
31
32
  { command: "cwd", description: "Show the current remote working directory" },
32
33
  { command: "runtime", description: "Show active runtime status" },
33
34
  { command: "room", description: "Show shared room status" },
@@ -207,6 +208,7 @@ function buildRemoteHelp() {
207
208
  "<code>/run &lt;prompt&gt;</code> execute an explicit remote request",
208
209
  "<code>/status</code> show the current linked remote session",
209
210
  "<code>/whoami</code> show your Telegram identity and room membership",
211
+ "<code>/people</code> list recent Telegram users in this chat for easier invites",
210
212
  "<code>/cwd</code> show the current remote working directory",
211
213
  "<code>/use &lt;path&gt;</code> switch the linked session to another directory",
212
214
  "<code>/desktop</code> switch the linked session to <code>~/Desktop</code>",
@@ -224,7 +226,8 @@ function buildRemoteHelp() {
224
226
  "<code>/task history &lt;id&gt;</code> show task history and comments",
225
227
  "<code>/room-runtime</code> or <code>/room-runtime &lt;name|clear&gt;</code> inspect or set the shared room runtime profile",
226
228
  "<code>/invites</code> list pending shared invites",
227
- "<code>/invite &lt;user-id&gt; [owner|editor|observer]</code> create a pending shared-project invite",
229
+ "<code>/invite &lt;user-id|@username&gt; [owner|editor|observer]</code> create a pending shared-project invite",
230
+ "reply to a teammate and use <code>/invite [owner|editor|observer]</code> to invite that person directly",
228
231
  "<code>/accept-invite &lt;invite-id&gt;</code> accept your own pending invite",
229
232
  "<code>/approve-invite &lt;invite-id&gt;</code> approve a pending invite",
230
233
  "<code>/reject-invite &lt;invite-id&gt;</code> reject a pending invite",
@@ -431,12 +434,40 @@ function formatTelegramWhoamiMarkup({ message, member = null, invites = [], shar
431
434
  lines.push("<b>Pending invites you can accept</b>");
432
435
  lines.push(...pending.map((invite) => `• <code>${escapeTelegramHtml(invite.id)}</code> <i>(${escapeTelegramHtml(invite.role || "editor")})</i>`));
433
436
  lines.push("Use <code>/accept-invite &lt;invite-id&gt;</code> to join the shared room.");
434
- } else if (!member) {
437
+ } else {
435
438
  lines.push("No pending Waterbrother room invite for this Telegram user id.");
436
439
  }
440
+ lines.push("Use <code>/people</code> in a shared chat to see recent Telegram ids and usernames you can invite.");
437
441
  return lines.join("\n");
438
442
  }
439
443
 
444
+ function formatTelegramPeopleMarkup({ people = [], project = null }) {
445
+ if (!people.length) {
446
+ return [
447
+ "<b>Recent Telegram people</b>",
448
+ "• none",
449
+ "Talk in the shared chat first, then run <code>/people</code> again."
450
+ ].join("\n");
451
+ }
452
+ const members = Array.isArray(project?.members) ? project.members : [];
453
+ const pendingInvites = Array.isArray(project?.pendingInvites) ? project.pendingInvites : [];
454
+ return [
455
+ "<b>Recent Telegram people</b>",
456
+ ...people.map((person) => {
457
+ const member = members.find((entry) => String(entry?.id || "").trim() === person.userId);
458
+ const pendingInvite = pendingInvites.find((entry) => String(entry?.memberId || "").trim() === person.userId);
459
+ const bits = [`<code>${escapeTelegramHtml(person.userId)}</code>`];
460
+ if (person.usernameHandle) bits.push(`@${escapeTelegramHtml(person.usernameHandle)}`);
461
+ bits.push(escapeTelegramHtml(person.displayName || person.userId));
462
+ if (member?.role) bits.push(`<i>member:${escapeTelegramHtml(member.role)}</i>`);
463
+ else if (pendingInvite?.id) bits.push(`<i>pending:${escapeTelegramHtml(pendingInvite.id)}</i>`);
464
+ else if (person.paired) bits.push("<i>paired</i>");
465
+ return `• ${bits.join(" ")}`;
466
+ }),
467
+ "Invite by id, by @username, or by replying to a message with <code>/invite [role]</code>."
468
+ ].join("\n");
469
+ }
470
+
440
471
  function formatTelegramTasksMarkup(tasks = []) {
441
472
  if (!tasks.length) {
442
473
  return "<b>Shared tasks</b>\n• none";
@@ -533,12 +564,15 @@ function suggestTaskText(text = "") {
533
564
 
534
565
  function parseInviteCommand(text) {
535
566
  const parts = String(text || "").trim().split(/\s+/).filter(Boolean);
536
- const userId = String(parts[1] || "").trim();
567
+ let target = String(parts[1] || "").trim();
537
568
  let role = "editor";
538
- if (parts[2] && ["owner", "editor", "observer"].includes(String(parts[2]).toLowerCase())) {
569
+ if (target && ["owner", "editor", "observer"].includes(target.toLowerCase())) {
570
+ role = target.toLowerCase();
571
+ target = "";
572
+ } else if (parts[2] && ["owner", "editor", "observer"].includes(String(parts[2]).toLowerCase())) {
539
573
  role = String(parts[2]).toLowerCase();
540
574
  }
541
- return { userId, role };
575
+ return { target, role };
542
576
  }
543
577
 
544
578
  function extractRetryDelayMs(error, attempt) {
@@ -723,12 +757,100 @@ class TelegramGateway {
723
757
  await saveGatewayState("telegram", this.state);
724
758
  }
725
759
 
760
+ describeTelegramUser(from = {}) {
761
+ const userId = String(from?.id || "").trim();
762
+ const usernameHandle = String(from?.username || "").trim();
763
+ const displayName = [from?.first_name, from?.last_name].filter(Boolean).join(" ").trim() || usernameHandle || userId;
764
+ return { userId, usernameHandle, displayName };
765
+ }
766
+
767
+ listKnownChatPeople(message) {
768
+ const chatId = String(message?.chat?.id || "").trim();
769
+ const byId = new Map();
770
+ const upsert = (person = {}) => {
771
+ const userId = String(person.userId || person.id || "").trim();
772
+ if (!userId) return;
773
+ const existing = byId.get(userId) || { userId, usernameHandle: "", displayName: userId, paired: false };
774
+ byId.set(userId, {
775
+ ...existing,
776
+ ...person,
777
+ userId,
778
+ usernameHandle: String(person.usernameHandle || existing.usernameHandle || "").trim(),
779
+ displayName: String(person.displayName || existing.displayName || userId).trim() || userId,
780
+ paired: person.paired === true || existing.paired === true
781
+ });
782
+ };
783
+
784
+ for (const [userId, peer] of Object.entries(this.state.peers || {})) {
785
+ if (String(peer?.chatId || "").trim() !== chatId) continue;
786
+ upsert({
787
+ userId,
788
+ usernameHandle: String(peer?.usernameHandle || "").trim(),
789
+ displayName: String(peer?.displayName || peer?.username || userId).trim(),
790
+ paired: true,
791
+ lastSeenAt: String(peer?.lastSeenAt || "").trim()
792
+ });
793
+ }
794
+
795
+ for (const [userId, pending] of Object.entries(this.state.pendingPairings || {})) {
796
+ if (String(pending?.chatId || "").trim() !== chatId) continue;
797
+ upsert({
798
+ userId,
799
+ usernameHandle: String(pending?.usernameHandle || "").trim(),
800
+ displayName: String(pending?.displayName || pending?.username || userId).trim(),
801
+ paired: false,
802
+ lastSeenAt: String(pending?.lastSeenAt || pending?.firstSeenAt || "").trim()
803
+ });
804
+ }
805
+
806
+ const current = this.describeTelegramUser(message?.from || {});
807
+ if (current.userId) upsert({ ...current, paired: this.isAuthorizedUser(message?.from) === true });
808
+ const replied = this.describeTelegramUser(message?.reply_to_message?.from || {});
809
+ if (replied.userId) upsert({ ...replied, paired: this.isAuthorizedUser(message?.reply_to_message?.from) === true });
810
+
811
+ return [...byId.values()].sort((a, b) => {
812
+ const aSeen = Date.parse(a.lastSeenAt || "") || 0;
813
+ const bSeen = Date.parse(b.lastSeenAt || "") || 0;
814
+ return bSeen - aSeen || a.displayName.localeCompare(b.displayName);
815
+ });
816
+ }
817
+
818
+ resolveInviteTarget(message, target) {
819
+ const value = String(target || "").trim();
820
+ const known = this.listKnownChatPeople(message);
821
+ const replied = this.describeTelegramUser(message?.reply_to_message?.from || {});
822
+
823
+ if (!value) {
824
+ if (replied.userId) {
825
+ const match = known.find((person) => person.userId === replied.userId) || replied;
826
+ return { userId: replied.userId, displayName: match.displayName || replied.displayName };
827
+ }
828
+ throw new Error("Usage: /invite <user-id|@username> [owner|editor|observer], or reply to someone with /invite [role]");
829
+ }
830
+
831
+ if (/^\d+$/.test(value)) {
832
+ const match = known.find((person) => person.userId === value);
833
+ return { userId: value, displayName: match?.displayName || value };
834
+ }
835
+
836
+ const normalized = value.replace(/^@/, "").trim().toLowerCase();
837
+ const byUsername = known.filter((person) => String(person.usernameHandle || "").trim().toLowerCase() === normalized);
838
+ if (byUsername.length === 1) {
839
+ return { userId: byUsername[0].userId, displayName: byUsername[0].displayName || byUsername[0].userId };
840
+ }
841
+ if (byUsername.length > 1) {
842
+ throw new Error(`Multiple Telegram users matched @${normalized}. Use /people and invite by numeric id.`);
843
+ }
844
+ throw new Error(`Unknown Telegram user reference: ${value}. Use /people to list recent users in this chat.`);
845
+ }
846
+
726
847
  async addPendingPairing(message) {
727
- const userId = String(message?.from?.id || "").trim();
728
- const username = [message?.from?.username, message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim();
848
+ const { userId, usernameHandle, displayName } = this.describeTelegramUser(message?.from || {});
729
849
  this.state.pendingPairings[userId] = {
730
850
  userId,
731
- username,
851
+ username: displayName,
852
+ usernameHandle,
853
+ displayName,
732
854
  chatId: String(message.chat.id),
733
855
  firstSeenAt: this.state.pendingPairings[userId]?.firstSeenAt || new Date().toISOString(),
734
856
  lastSeenAt: new Date().toISOString(),
@@ -763,14 +885,15 @@ class TelegramGateway {
763
885
  }
764
886
 
765
887
  async ensurePeerSession(message) {
766
- const userId = String(message?.from?.id || "").trim();
767
- const username = [message?.from?.username, message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim();
888
+ const { userId, usernameHandle, displayName } = this.describeTelegramUser(message?.from || {});
768
889
  const existing = this.getPeerState(userId);
769
890
  if (existing?.sessionId) {
770
891
  this.state.peers[userId] = {
771
892
  ...existing,
772
893
  chatId: String(message.chat.id),
773
- username,
894
+ username: displayName,
895
+ usernameHandle,
896
+ displayName,
774
897
  sessions: upsertSessionHistory(existing.sessions, existing.sessionId),
775
898
  lastSeenAt: new Date().toISOString(),
776
899
  lastMessageId: message.message_id
@@ -793,7 +916,9 @@ class TelegramGateway {
793
916
  this.state.peers[userId] = {
794
917
  sessionId: session.id,
795
918
  chatId: String(message.chat.id),
796
- username,
919
+ username: displayName,
920
+ usernameHandle,
921
+ displayName,
797
922
  sessions: upsertSessionHistory([], session.id),
798
923
  linkedAt: new Date().toISOString(),
799
924
  lastSeenAt: new Date().toISOString(),
@@ -822,7 +947,9 @@ class TelegramGateway {
822
947
  ...previous,
823
948
  sessionId: session.id,
824
949
  chatId: String(message.chat.id),
825
- username: [message?.from?.username, message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim(),
950
+ username: this.describeTelegramUser(message?.from || {}).displayName,
951
+ usernameHandle: this.describeTelegramUser(message?.from || {}).usernameHandle,
952
+ displayName: this.describeTelegramUser(message?.from || {}).displayName,
826
953
  sessions: upsertSessionHistory(previous.sessions, session.id),
827
954
  linkedAt: new Date().toISOString(),
828
955
  lastSeenAt: new Date().toISOString(),
@@ -853,7 +980,9 @@ class TelegramGateway {
853
980
  ...previous,
854
981
  sessionId: session.id,
855
982
  chatId: String(message.chat.id),
856
- username: [message?.from?.username, message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim(),
983
+ username: this.describeTelegramUser(message?.from || {}).displayName,
984
+ usernameHandle: this.describeTelegramUser(message?.from || {}).usernameHandle,
985
+ displayName: this.describeTelegramUser(message?.from || {}).displayName,
857
986
  sessions: upsertSessionHistory(previous.sessions, session.id),
858
987
  linkedAt: previous.linkedAt || new Date().toISOString(),
859
988
  lastSeenAt: new Date().toISOString(),
@@ -977,7 +1106,9 @@ class TelegramGateway {
977
1106
  id: requestId,
978
1107
  chatId: String(message.chat.id),
979
1108
  userId: String(message?.from?.id || ""),
980
- username: [message?.from?.username, message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim(),
1109
+ username: this.describeTelegramUser(message?.from || {}).displayName,
1110
+ usernameHandle: this.describeTelegramUser(message?.from || {}).usernameHandle,
1111
+ displayName: this.describeTelegramUser(message?.from || {}).displayName,
981
1112
  sessionId: String(sessionId || "").trim(),
982
1113
  text: String(promptText || "").trim(),
983
1114
  runtimeProfile: String(project?.runtimeProfile || "").trim(),
@@ -1215,6 +1346,16 @@ class TelegramGateway {
1215
1346
  return;
1216
1347
  }
1217
1348
 
1349
+ if (text === "/people") {
1350
+ const { project } = await this.bindSharedRoomForMessage(message, sessionId);
1351
+ await this.sendMessage(
1352
+ message.chat.id,
1353
+ formatTelegramPeopleMarkup({ people: this.listKnownChatPeople(message), project }),
1354
+ message.message_id
1355
+ );
1356
+ return;
1357
+ }
1358
+
1218
1359
  if (text === "/status") {
1219
1360
  await this.sendMessage(
1220
1361
  message.chat.id,
@@ -1394,20 +1535,18 @@ class TelegramGateway {
1394
1535
  await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
1395
1536
  return;
1396
1537
  }
1397
- const { userId: nextUserId, role } = parseInviteCommand(text);
1398
- if (!nextUserId) {
1399
- await this.sendMessage(message.chat.id, "Usage: /invite <user-id> [owner|editor|observer]", message.message_id);
1400
- return;
1401
- }
1538
+ const { target, role } = parseInviteCommand(text);
1402
1539
  try {
1540
+ const inviteTarget = this.resolveInviteTarget(message, target);
1403
1541
  const result = await createSharedInvite(
1404
1542
  session.cwd || this.cwd,
1405
- { id: nextUserId, role, name: nextUserId, paired: true },
1543
+ { id: inviteTarget.userId, role, name: inviteTarget.displayName || inviteTarget.userId, paired: true },
1406
1544
  { actorId: userId, actorName: peer?.username || [message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim() || userId }
1407
1545
  );
1408
1546
  await this.sendMessage(
1409
1547
  message.chat.id,
1410
- `Created pending invite <code>${escapeTelegramHtml(result.invite.id)}</code> for <code>${escapeTelegramHtml(result.invite.memberId)}</code> <i>(${escapeTelegramHtml(result.invite.role)})</i>`,
1548
+ `Created pending invite <code>${escapeTelegramHtml(result.invite.id)}</code> for <code>${escapeTelegramHtml(result.invite.memberId)}</code> <i>(${escapeTelegramHtml(result.invite.role)})</i>
1549
+ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTelegramHtml(result.invite.id)}</code>.`,
1411
1550
  message.message_id
1412
1551
  );
1413
1552
  } catch (error) {
@@ -98,6 +98,223 @@ function buildProjectMapSummary({ entries = [], packageJson = null, sharedProjec
98
98
  };
99
99
  }
100
100
 
101
+ function collectSources(...groups) {
102
+ return [...new Set(groups.flat().filter(Boolean))];
103
+ }
104
+
105
+ function commandEntry(id, summary, commands = [], options = {}) {
106
+ return {
107
+ id,
108
+ summary,
109
+ commands,
110
+ surfaces: options.surfaces || ["shell", "tui", "telegram"],
111
+ keywords: options.keywords || [id],
112
+ sources: options.sources || []
113
+ };
114
+ }
115
+
116
+ function capabilityEntry(id, summary, options = {}) {
117
+ return {
118
+ id,
119
+ summary,
120
+ keywords: options.keywords || [id],
121
+ sources: options.sources || []
122
+ };
123
+ }
124
+
125
+ function workflowEntry(id, summary, steps = [], options = {}) {
126
+ return {
127
+ id,
128
+ summary,
129
+ steps,
130
+ keywords: options.keywords || [id],
131
+ sources: options.sources || []
132
+ };
133
+ }
134
+
135
+ function buildCommandRegistry({ sourceHints = {}, sharedRoom = {} } = {}) {
136
+ return [
137
+ commandEntry("share-project", "Enable shared-project mode in the current repo and create Roundtable state files.", [
138
+ "waterbrother project share",
139
+ "/share-project"
140
+ ], { keywords: ["share project", "shared project", "enable roundtable", "enable shared room"], sources: [sourceHints.roundtableDocs, sourceHints.sharedState] }),
141
+ commandEntry("room-status", "Inspect the shared room, recent events, members, invites, tasks, and runtime.", [
142
+ "waterbrother room status",
143
+ "waterbrother room events",
144
+ "/room",
145
+ "/events"
146
+ ], { keywords: ["roundtable", "shared room", "room status", "events"], sources: [sourceHints.roundtableDocs, sourceHints.sharedState] }),
147
+ commandEntry("room-membership", "Manage shared-room invites and membership.", [
148
+ "waterbrother room invite <member-id> [owner|editor|observer]",
149
+ "waterbrother room invite accept <invite-id>",
150
+ "/invite <user-id|@username> [owner|editor|observer]",
151
+ "/accept-invite <invite-id>",
152
+ "/whoami",
153
+ "/people"
154
+ ], { keywords: ["invite", "partner", "teammate", "collaborator", "member", "accept invite"], sources: [sourceHints.roundtableDocs, sourceHints.telegramDocs, sourceHints.sharedState] }),
155
+ commandEntry("room-mode", "Set whether a shared room is chatting, planning, or executing.", [
156
+ "waterbrother room mode <chat|plan|execute>",
157
+ "/mode <chat|plan|execute>",
158
+ "/run <prompt>"
159
+ ], { keywords: ["room mode", "execute mode", "plan mode", "chat mode", "group execution"], sources: [sourceHints.roundtableDocs, sourceHints.telegramDocs] }),
160
+ commandEntry("room-operator", "Claim or release the active operator lock for shared execution.", [
161
+ "waterbrother room claim",
162
+ "waterbrother room release",
163
+ "/claim",
164
+ "/release"
165
+ ], { keywords: ["claim", "release", "operator", "lock", "active operator"], sources: [sourceHints.roundtableDocs, sourceHints.sharedState] }),
166
+ commandEntry("room-tasks", "Inspect and manage the shared Roundtable task queue.", [
167
+ "waterbrother room tasks",
168
+ "waterbrother room task add <text>",
169
+ "waterbrother room task assign <id> <member-id>",
170
+ "waterbrother room task move <id> <open|active|blocked|done>",
171
+ "/tasks",
172
+ "/task add <text>",
173
+ "/task assign <id> <member-id>",
174
+ "/task claim <id>",
175
+ "/task move <id> <open|active|blocked|done>"
176
+ ], { keywords: ["task", "tasks", "backlog", "assign task", "roundtable task"], sources: [sourceHints.roundtableDocs, sourceHints.sharedState] }),
177
+ commandEntry("new-project", "Create a new project and switch the live session into it.", [
178
+ "/new-project <name>",
179
+ "/desktop",
180
+ "/use <path>"
181
+ ], { keywords: ["new project", "make project", "create project", "desktop project"], sources: [sourceHints.readme, sourceHints.telegramDocs] }),
182
+ commandEntry("runtime-profiles", "Inspect or select named runtime presets for local and shared execution.", [
183
+ "waterbrother runtime-profiles list",
184
+ "waterbrother room runtime [name|clear]",
185
+ "/runtime-profiles",
186
+ "/room-runtime <name|clear>"
187
+ ], { keywords: ["runtime profile", "runtime profiles", "provider preset", "shared room runtime"], sources: [sourceHints.readme, sourceHints.roundtableDocs] }),
188
+ commandEntry("telegram-gateway", "Inspect and run the Telegram messaging gateway.", [
189
+ "waterbrother gateway status",
190
+ "waterbrother gateway run telegram",
191
+ "waterbrother gateway stop",
192
+ "/gateway"
193
+ ], { keywords: ["telegram", "gateway", "messaging", "telegram adapter"], sources: [sourceHints.telegramDocs] }),
194
+ commandEntry("self-awareness", "Inspect Waterbrother's own repo/runtime understanding.", [
195
+ "waterbrother about",
196
+ "waterbrother capabilities",
197
+ "waterbrother project-map",
198
+ "waterbrother state",
199
+ "/about",
200
+ "/capabilities",
201
+ "/project-map",
202
+ "/state"
203
+ ], { keywords: ["about", "capabilities", "state", "project map", "self awareness", "what are you"], sources: [sourceHints.readme, sourceHints.commandsDocs] })
204
+ ];
205
+ }
206
+
207
+ function buildCapabilityRegistry({ sourceHints = {}, sharedRoom = {} } = {}) {
208
+ const sharedEnabled = sharedRoom.enabled === true;
209
+ return [
210
+ capabilityEntry("interactive-terminal", "Interactive terminal coding with sessions, approvals, tools, receipts, and local repo awareness.", { keywords: ["terminal", "tui", "interactive", "coding cli"], sources: [sourceHints.readme] }),
211
+ capabilityEntry("telegram-bridge", `Telegram messaging gateway with ${sharedEnabled ? "live TUI bridging and shared-room controls" : "pairing, remote sessions, and shared-room support when enabled"}.`, { keywords: ["telegram", "bot", "bridge", "remote control", "messaging"], sources: [sourceHints.telegramDocs] }),
212
+ capabilityEntry("shared-projects", `${sharedEnabled ? "Shared-project collaboration is enabled in this repo." : "Shared-project collaboration is available and currently disabled in this repo."} It uses one room, explicit roles, room modes, an operator lock, Roundtable tasks, and a shared runtime.`, { keywords: ["roundtable", "shared project", "shared room", "collaboration"], sources: [sourceHints.roundtableDocs, sourceHints.sharedState] }),
213
+ capabilityEntry("runtime-profiles", "Bring-your-own-model runtime switching with named runtime profiles and room-level shared runtime selection.", { keywords: ["runtime", "profiles", "provider", "model", "byom"], sources: [sourceHints.readme, sourceHints.roundtableDocs] }),
214
+ capabilityEntry("session-persistence", "Persisted sessions that store cwd, runtime state, and messages for local and Telegram resumes.", { keywords: ["session", "sessions", "resume", "history"], sources: [sourceHints.readme, sourceHints.sharedState] }),
215
+ capabilityEntry("self-awareness", "Local self-awareness generated from repo state, docs, product metadata, shared-room state, and live runtime context.", { keywords: ["self awareness", "know itself", "what are you", "harness", "code base"], sources: [sourceHints.readme, sourceHints.sharedState, sourceHints.commandsDocs] })
216
+ ];
217
+ }
218
+
219
+ function buildWorkflowRegistry({ sourceHints = {} } = {}) {
220
+ return [
221
+ workflowEntry("invite-partner", "Add a partner to a shared Telegram collaboration room.", [
222
+ "Add the human to the Telegram group/chat itself using normal Telegram controls.",
223
+ "Enable shared-project mode with waterbrother project share if the repo is not shared yet.",
224
+ "Use /people or /whoami to discover Telegram ids already seen in the room.",
225
+ "Create a Waterbrother shared-room invite with waterbrother room invite <member-id> [owner|editor|observer] or /invite <user-id|@username> [owner|editor|observer].",
226
+ "Have the invited person accept with waterbrother room invite accept <invite-id> or /accept-invite <invite-id>."
227
+ ], { keywords: ["invite partner", "invite teammate", "add collaborator", "join shared room"], sources: [sourceHints.roundtableDocs, sourceHints.telegramDocs, sourceHints.sharedState] }),
228
+ workflowEntry("make-project", "Create a new project and optionally make it collaborative.", [
229
+ "Use /new-project <name> in the TUI or Telegram to create a Desktop project and switch into it.",
230
+ "If prompted in the TUI, choose whether the new project should be shared.",
231
+ "Use waterbrother project share afterward if you want to enable Roundtable on an existing repo.",
232
+ "Set room mode and claim the operator lock before shared execution."
233
+ ], { keywords: ["make new project", "create project", "new project", "start project"], sources: [sourceHints.readme, sourceHints.roundtableDocs, sourceHints.telegramDocs] }),
234
+ workflowEntry("shared-execution", "Run shared work safely in Telegram groups.", [
235
+ "Use /mode execute for the shared room when the group is ready to act.",
236
+ "Owners or editors claim the operator lock with /claim or waterbrother room claim.",
237
+ "In Telegram groups, explicit execution should use /run <prompt>.",
238
+ "Use /events, /room, and /tasks to keep the group synchronized."
239
+ ], { keywords: ["shared execution", "group execute", "work together in telegram", "run in group"], sources: [sourceHints.roundtableDocs, sourceHints.telegramDocs, sourceHints.sharedState] })
240
+ ];
241
+ }
242
+
243
+ function summarizeRoomState(sharedRoom = {}) {
244
+ if (!sharedRoom.enabled) {
245
+ return "Shared room is off in this repo.";
246
+ }
247
+ const bits = [`Shared room is enabled in ${sharedRoom.roomMode || "chat"} mode.`];
248
+ if (sharedRoom.runtimeProfile) bits.push(`Room runtime profile: ${sharedRoom.runtimeProfile}.`);
249
+ bits.push(`Members: ${sharedRoom.memberCount || 0}, pending invites: ${sharedRoom.pendingInviteCount || 0}, tasks: ${sharedRoom.taskCount || 0}.`);
250
+ if (sharedRoom.activeOperator?.id) bits.push(`Active operator: ${sharedRoom.activeOperator.name || sharedRoom.activeOperator.id}.`);
251
+ return bits.join(" ");
252
+ }
253
+
254
+ function normalizeQuestion(text = "") {
255
+ return String(text || "")
256
+ .toLowerCase()
257
+ .replace(/[^a-z0-9\s/-]+/g, " ")
258
+ .replace(/\s+/g, " ")
259
+ .trim();
260
+ }
261
+
262
+ function scoreEntry(question, entry) {
263
+ const q = normalizeQuestion(question);
264
+ const qTokens = new Set(q.split(" ").filter(Boolean));
265
+ let score = 0;
266
+ for (const keyword of entry.keywords || []) {
267
+ const normalized = normalizeQuestion(keyword);
268
+ if (!normalized) continue;
269
+ if (q.includes(normalized)) score += Math.max(2, normalized.split(" ").length * 2);
270
+ const tokens = normalized.split(" ").filter(Boolean);
271
+ if (tokens.length > 1 && tokens.every((token) => qTokens.has(token))) {
272
+ score += tokens.length + 1;
273
+ }
274
+ }
275
+ if (entry.id && q.includes(normalizeQuestion(entry.id))) score += 2;
276
+ return score;
277
+ }
278
+
279
+ function findBestEntries(question, entries = [], minimumScore = 2) {
280
+ return entries
281
+ .map((entry) => ({ entry, score: scoreEntry(question, entry) }))
282
+ .filter((item) => item.score >= minimumScore)
283
+ .sort((a, b) => b.score - a.score)
284
+ .map((item) => item.entry);
285
+ }
286
+
287
+ function renderWorkflowAnswer(workflow, roomState, capability, commands) {
288
+ const lines = [workflow.summary, ...workflow.steps.map((step, index) => `${index + 1}. ${step}`)];
289
+ if (commands.length) {
290
+ lines.push("Relevant commands:");
291
+ for (const command of commands.slice(0, 2)) {
292
+ lines.push(...command.commands.map((value) => `- ${value}`));
293
+ }
294
+ }
295
+ if (roomState) lines.push(roomState);
296
+ if (capability) lines.push(`Context: ${capability.summary}`);
297
+ const sources = collectSources(workflow.sources, capability?.sources || [], ...commands.map((entry) => entry.sources || []));
298
+ if (sources.length) lines.push(`sources: ${sources.join(", ")}`);
299
+ return lines.join("\n");
300
+ }
301
+
302
+ function renderConceptAnswer({ subject, capability, commands = [], roomState, extra = [] }) {
303
+ const lines = [subject];
304
+ if (capability) lines.push(capability.summary);
305
+ if (roomState) lines.push(roomState);
306
+ if (commands.length) {
307
+ lines.push("Relevant commands:");
308
+ for (const entry of commands.slice(0, 3)) {
309
+ lines.push(...entry.commands.map((value) => `- ${value}`));
310
+ }
311
+ }
312
+ if (extra.length) lines.push(...extra);
313
+ const sources = collectSources(capability?.sources || [], ...commands.map((entry) => entry.sources || []));
314
+ if (sources.length) lines.push(`sources: ${sources.join(", ")}`);
315
+ return lines.join("\n");
316
+ }
317
+
101
318
  export async function buildSelfAwarenessManifest({ cwd, runtime = {}, currentSession = null } = {}) {
102
319
  const [packageJson, readme, memoryText, roundtableText, sharedProject, product, git, entries] = await Promise.all([
103
320
  readJson(path.join(cwd, "package.json")),
@@ -117,6 +334,26 @@ export async function buildSelfAwarenessManifest({ cwd, runtime = {}, currentSes
117
334
  };
118
335
 
119
336
  const projectMap = buildProjectMapSummary({ entries, packageJson, sharedProject, product });
337
+ const sourceHints = {
338
+ readme: await exists(path.join(cwd, "README.md")) ? "README.md" : "",
339
+ projectMemory: await exists(path.join(cwd, "WATERBROTHER.md")) ? "WATERBROTHER.md" : "",
340
+ roundtable: await exists(path.join(cwd, "ROUNDTABLE.md")) ? "ROUNDTABLE.md" : "",
341
+ sharedState: sharedProject ? ".waterbrother/shared.json" : "",
342
+ telegramDocs: docsPresence.telegram ? "channels/telegram/index.html" : "",
343
+ roundtableDocs: docsPresence.roundtable ? "roundtable/index.html" : "",
344
+ commandsDocs: await exists(path.join(cwd, "commands", "index.html")) ? "commands/index.html" : ""
345
+ };
346
+ const sharedRoom = sharedProject
347
+ ? {
348
+ enabled: true,
349
+ roomMode: sharedProject.roomMode || "chat",
350
+ runtimeProfile: sharedProject.runtimeProfile || "",
351
+ memberCount: Array.isArray(sharedProject.members) ? sharedProject.members.length : 0,
352
+ pendingInviteCount: Array.isArray(sharedProject.pendingInvites) ? sharedProject.pendingInvites.length : 0,
353
+ taskCount: Array.isArray(sharedProject.tasks) ? sharedProject.tasks.length : 0,
354
+ activeOperator: sharedProject.activeOperator || null
355
+ }
356
+ : { enabled: false };
120
357
  const manifest = {
121
358
  version: 1,
122
359
  generatedAt: new Date().toISOString(),
@@ -154,17 +391,7 @@ export async function buildSelfAwarenessManifest({ cwd, runtime = {}, currentSes
154
391
  roundtableTitle: firstHeading(roundtableText) || "",
155
392
  projectMap
156
393
  },
157
- sharedRoom: sharedProject
158
- ? {
159
- enabled: true,
160
- roomMode: sharedProject.roomMode || "chat",
161
- runtimeProfile: sharedProject.runtimeProfile || "",
162
- memberCount: Array.isArray(sharedProject.members) ? sharedProject.members.length : 0,
163
- pendingInviteCount: Array.isArray(sharedProject.pendingInvites) ? sharedProject.pendingInvites.length : 0,
164
- taskCount: Array.isArray(sharedProject.tasks) ? sharedProject.tasks.length : 0,
165
- activeOperator: sharedProject.activeOperator || null
166
- }
167
- : { enabled: false },
394
+ sharedRoom,
168
395
  product: product
169
396
  ? {
170
397
  name: product.name || "",
@@ -172,13 +399,18 @@ export async function buildSelfAwarenessManifest({ cwd, runtime = {}, currentSes
172
399
  surfaceCount: Array.isArray(product.surfaces) ? product.surfaces.length : 0
173
400
  }
174
401
  : null,
175
- sourceHints: {
176
- readme: await exists(path.join(cwd, "README.md")) ? "README.md" : "",
177
- projectMemory: await exists(path.join(cwd, "WATERBROTHER.md")) ? "WATERBROTHER.md" : "",
178
- roundtable: await exists(path.join(cwd, "ROUNDTABLE.md")) ? "ROUNDTABLE.md" : "",
179
- sharedState: sharedProject ? ".waterbrother/shared.json" : "",
180
- telegramDocs: docsPresence.telegram ? "channels/telegram/index.html" : "",
181
- roundtableDocs: docsPresence.roundtable ? "roundtable/index.html" : ""
402
+ sourceHints,
403
+ knowledge: {
404
+ harness: {
405
+ surface: "waterbrother",
406
+ runtimeSurface: "local terminal",
407
+ messagingSurfaces: ["telegram"],
408
+ liveBridge: true,
409
+ sharedRoomSummary: summarizeRoomState(sharedRoom)
410
+ },
411
+ commands: buildCommandRegistry({ sourceHints, sharedRoom }),
412
+ capabilities: buildCapabilityRegistry({ sourceHints, sharedRoom }),
413
+ workflows: buildWorkflowRegistry({ sourceHints })
182
414
  }
183
415
  };
184
416
 
@@ -202,7 +434,8 @@ export function buildSelfAwarenessMemoryBlock(manifest = {}) {
202
434
  `- git branch: ${manifest.identity?.git?.branch || "none"}`,
203
435
  `- runtime: ${manifest.runtime?.provider || "unknown"}/${manifest.runtime?.model || "unknown"}`,
204
436
  `- shared room: ${manifest.sharedRoom?.enabled ? `${manifest.sharedRoom.roomMode || "chat"} mode` : "off"}`,
205
- `- capabilities: ${(manifest.features?.capabilities || []).join(", ")}`
437
+ `- capabilities: ${(manifest.features?.capabilities || []).join(", ")}`,
438
+ `- harness: ${manifest.knowledge?.harness?.sharedRoomSummary || "local terminal"}`
206
439
  ];
207
440
  return lines.filter(Boolean).join("\n");
208
441
  }
@@ -210,7 +443,8 @@ export function buildSelfAwarenessMemoryBlock(manifest = {}) {
210
443
  export function formatCapabilitiesSummary(manifest = {}) {
211
444
  return JSON.stringify({
212
445
  identity: manifest.identity,
213
- capabilities: manifest.features?.capabilities || [],
446
+ capabilities: manifest.knowledge?.capabilities || manifest.features?.capabilities || [],
447
+ commands: (manifest.knowledge?.commands || []).map((entry) => ({ id: entry.id, commands: entry.commands })),
214
448
  channels: manifest.features?.channels || [],
215
449
  sharedRoomEnabled: manifest.sharedRoom?.enabled === true
216
450
  }, null, 2);
@@ -239,13 +473,15 @@ export function formatAboutWaterbrother(manifest = {}) {
239
473
  manifest.sourceHints?.readme,
240
474
  manifest.sourceHints?.roundtableDocs,
241
475
  manifest.sourceHints?.telegramDocs,
242
- manifest.sourceHints?.sharedState
476
+ manifest.sourceHints?.sharedState,
477
+ manifest.sourceHints?.commandsDocs
243
478
  ].filter(Boolean);
244
479
  return [
245
480
  `Waterbrother is ${manifest.identity?.description || "a local coding CLI."}`,
246
481
  `repo: ${manifest.identity?.repoName || "-"}`,
247
482
  `cwd: ${manifest.identity?.cwd || "-"}`,
248
483
  `capabilities: ${(manifest.features?.capabilities || []).join(", ") || "-"}`,
484
+ `harness: ${manifest.knowledge?.harness?.sharedRoomSummary || "local terminal"}`,
249
485
  sources.length ? `sources: ${sources.join(", ")}` : ""
250
486
  ].filter(Boolean).join("\n");
251
487
  }
@@ -255,121 +491,76 @@ export function resolveLocalConceptQuestion(text = "", manifest = {}) {
255
491
  const lower = input.toLowerCase();
256
492
  if (!lower) return null;
257
493
 
258
- const mentionsRoundtable = /\bround[\s-]?table\b/.test(lower);
259
494
  const asksWhatIs = /^(what('| i)?s|what is|tell me about|explain)\b/.test(lower) || /\bwhat is\b/.test(lower);
260
- const asksHowToUseRoundtable = mentionsRoundtable && /\b(how do i|how to|add to|join|use|set up|enable|create)\b/.test(lower);
261
- const asksHowToInvitePartner = /\b(invite|add)\b/.test(lower) && /\b(partner|teammate|teammates|teammember|collaborator|member)\b/.test(lower);
262
- const asksHowToShareProject = /\b(how do i|how to|share)\b/.test(lower) && /\b(this project|project)\b/.test(lower);
263
- const asksHowToMakeProject = /\b(how do i|how to|make|create|start)\b/.test(lower) && /\b(new project|project)\b/.test(lower);
264
- const sourcesFor = (...keys) => keys.map((key) => manifest.sourceHints?.[key]).filter(Boolean);
265
- const withSources = (lines, sources = []) => [
266
- ...lines,
267
- ...(sources.length ? [`sources: ${sources.join(", ")}`] : [])
268
- ].join("\n");
269
- if (mentionsRoundtable && asksWhatIs) {
270
- const sources = sourcesFor("roundtableDocs", "sharedState", "roundtable");
271
- return withSources([
272
- "Roundtable is Waterbrother’s shared-project collaboration model.",
273
- "It ties one project to one shared room with explicit room modes, member roles, operator lock, tasks, invites, and shared runtime selection."
274
- ], sources);
275
- }
276
-
277
- if (asksHowToUseRoundtable) {
278
- const sources = sourcesFor("roundtableDocs", "sharedState", "roundtable");
279
- return withSources([
280
- "To add to Roundtable, first enable shared-project mode in the repo with `waterbrother project share`.",
281
- "Then use `waterbrother room invite <member-id> [owner|editor|observer]` to create member invites, `waterbrother room tasks` / `waterbrother room task add <text>` to work the shared backlog, and `waterbrother room mode <chat|plan|execute>` plus `waterbrother room claim` when you want one operator to execute.",
282
- "In Telegram, the equivalent shared-room commands are `/room`, `/invite`, `/tasks`, `/task add`, `/mode`, `/claim`, and `/accept-invite`."
283
- ], sources);
284
- }
285
-
286
- if (asksHowToInvitePartner) {
287
- const sources = sourcesFor("roundtableDocs", "telegramDocs", "sharedState");
288
- return withSources([
289
- "To collaborate with a Telegram partner, there are two separate steps.",
290
- "First, add the human to the Telegram group/chat itself using normal Telegram controls.",
291
- "Second, add them to the Waterbrother shared room with `waterbrother room invite <member-id> [owner|editor|observer]` or `/invite <user-id> [owner|editor|observer]`.",
292
- "Then the invited person accepts with `waterbrother room invite accept <invite-id>` or `/accept-invite <invite-id>`.",
293
- "If this repo is not shared yet, start with `waterbrother project share`."
294
- ], sources);
295
- }
296
-
297
- if (asksHowToShareProject) {
298
- const sources = sourcesFor("roundtableDocs", "sharedState");
299
- return withSources([
300
- "To share the current project, run `waterbrother project share` in the repo.",
301
- "In an interactive terminal, Waterbrother will guide room mode, room runtime profile, and the first invite.",
302
- "After that, use `waterbrother room status`, `waterbrother room invite <member-id> [owner|editor|observer]`, and `waterbrother room mode <chat|plan|execute>` to manage collaboration."
303
- ], sources);
304
- }
495
+ const asksHow = /\b(how do i|how to|use|set up|enable|create|share|join|invite|make|start|add)\b/.test(lower);
496
+ const asksCapabilities = /\bwhat can you do\b/.test(lower) || /\bcapabilities\b/.test(lower) || /\bwhat do you support\b/.test(lower);
497
+ const asksProjectMap = /\bwhat is this project\b/.test(lower) || /\bwhat repo is this\b/.test(lower) || /\bwhat codebase is this\b/.test(lower);
498
+ const asksIdentity = /\bwhat is waterbrother\b/.test(lower) || /\bwho are you\b/.test(lower) || /\bwhat are you\b/.test(lower);
499
+ const mentionsRoundtable = /\bround[\s-]?table\b/.test(lower);
500
+ const mentionsSharedRoom = /\bshared room\b/.test(lower) || /\bshared project\b/.test(lower);
305
501
 
306
- if (asksHowToMakeProject) {
307
- const sources = sourcesFor("roundtableDocs", "telegramDocs", "readme");
308
- return withSources([
309
- "To make a new project from the TUI, use `/new-project <name>`.",
310
- "That creates a folder on `~/Desktop/<name>`, switches the live session into it, and now asks whether the new project should be shared.",
311
- "From Telegram, use `/new-project <name>` to create the folder and switch the linked session there. Then use `waterbrother project share` or the TUI sharing prompt if you want collaboration enabled."
312
- ], sources);
313
- }
502
+ const commands = manifest.knowledge?.commands || buildCommandRegistry({ sourceHints: manifest.sourceHints || {}, sharedRoom: manifest.sharedRoom || {} });
503
+ const capabilities = manifest.knowledge?.capabilities || buildCapabilityRegistry({ sourceHints: manifest.sourceHints || {}, sharedRoom: manifest.sharedRoom || {} });
504
+ const workflows = manifest.knowledge?.workflows || buildWorkflowRegistry({ sourceHints: manifest.sourceHints || {} });
505
+ const roomState = manifest.knowledge?.harness?.sharedRoomSummary || summarizeRoomState(manifest.sharedRoom || {});
506
+ const matchingWorkflows = findBestEntries(input, workflows, asksHow ? 2 : 4);
507
+ const matchingCommands = findBestEntries(input, commands, 2);
508
+ const matchingCapabilities = findBestEntries(input, capabilities, 2);
314
509
 
315
- if ((/\bshared project\b/.test(lower) || /\bshared room\b/.test(lower) || /\broom mode\b/.test(lower)) && asksWhatIs) {
316
- const sources = sourcesFor("sharedState", "roundtableDocs", "roundtable");
317
- return withSources([
318
- "A shared project links the current repo to one collaboration room with explicit member roles, room mode, operator lock, tasks, invites, and shared runtime state.",
319
- `Current room mode: ${manifest.sharedRoom?.roomMode || "chat"}. Shared enabled: ${manifest.sharedRoom?.enabled ? "yes" : "no"}.`
320
- ], sources);
321
- }
322
-
323
- if ((/\bwhat can you do\b/.test(lower) || /\bcapabilities\b/.test(lower) || /\bwhat do you support\b/.test(lower))) {
510
+ if (asksCapabilities) {
324
511
  return formatCapabilitiesSummary(manifest);
325
512
  }
326
513
 
327
- if ((/\bwhat is this project\b/.test(lower) || /\bwhat repo is this\b/.test(lower) || /\bwhat codebase is this\b/.test(lower))) {
514
+ if (asksProjectMap) {
328
515
  return formatProjectMap(manifest);
329
516
  }
330
517
 
331
- if (/\bwhat is waterbrother\b/.test(lower) || /\bwho are you\b/.test(lower) || /\bwhat are you\b/.test(lower)) {
518
+ if (asksIdentity) {
332
519
  return formatAboutWaterbrother(manifest);
333
520
  }
334
521
 
335
- if (/\btelegram\b/.test(lower) && asksWhatIs) {
336
- const sources = sourcesFor("telegramDocs", "sharedState");
337
- return withSources([
338
- "Telegram is Waterbrother’s first live messaging adapter.",
339
- "It supports pairing, live TUI bridging, shared-room commands, task operations, and room execution gating."
340
- ], sources);
341
- }
342
-
343
- if ((/\bgateway\b/.test(lower) || /\bmessaging gateway\b/.test(lower)) && asksWhatIs) {
344
- const sources = sourcesFor("telegramDocs", "sharedState");
345
- return withSources([
346
- "The gateway is Waterbrother’s channel runtime for messaging surfaces like Telegram.",
347
- "It handles pairing, routing, live TUI bridging, remote sessions, shared-room commands, and delivery state."
348
- ], sources);
522
+ if (asksWhatIs && mentionsRoundtable) {
523
+ return renderConceptAnswer({
524
+ subject: "Roundtable is Waterbrother's shared-project collaboration model.",
525
+ capability: matchingCapabilities[0] || capabilities.find((entry) => entry.id === "shared-projects") || null,
526
+ commands: matchingCommands.length ? matchingCommands : commands.filter((entry) => ["room-status", "room-membership", "room-tasks"].includes(entry.id)),
527
+ roomState,
528
+ extra: ["It ties one project to one room with explicit roles, room modes, an operator lock, Roundtable tasks, and shared runtime state."]
529
+ });
349
530
  }
350
531
 
351
- if ((/\bruntime profile\b/.test(lower) || /\bruntime profiles\b/.test(lower)) && asksWhatIs) {
352
- const sources = sourcesFor("readme", "sharedState");
353
- return withSources([
354
- "Runtime profiles are saved Waterbrother provider/model/runtime configurations.",
355
- "A shared room can also pin a room-level runtime profile that Telegram execution inherits when present."
356
- ], sources);
532
+ if (asksWhatIs && mentionsSharedRoom) {
533
+ return renderConceptAnswer({
534
+ subject: "A shared project links the current repo to one collaboration room with explicit shared-room state.",
535
+ capability: matchingCapabilities[0] || capabilities.find((entry) => entry.id === "shared-projects") || null,
536
+ commands: matchingCommands.length ? matchingCommands : commands.filter((entry) => ["room-status", "room-mode", "room-operator"].includes(entry.id)),
537
+ roomState,
538
+ extra: ["That shared room carries member roles, room mode, operator lock, tasks, invites, and shared runtime state."]
539
+ });
357
540
  }
358
541
 
359
- if ((/\bapproval\b/.test(lower) || /\bapprovals\b/.test(lower)) && asksWhatIs) {
360
- const sources = sourcesFor("readme", "telegramDocs");
361
- return withSources([
362
- "Approvals are Waterbrother’s safety gate for risky actions and scope changes.",
363
- "Local TUI runs can use the normal approval flow, while Telegram fallback runs still use approval=never when no live TUI host is attached."
364
- ], sources);
542
+ if (asksHow && matchingWorkflows.length) {
543
+ return renderWorkflowAnswer(
544
+ matchingWorkflows[0],
545
+ roomState,
546
+ matchingCapabilities[0] || null,
547
+ matchingCommands
548
+ );
365
549
  }
366
550
 
367
- if ((/\bsession\b/.test(lower) || /\bsessions\b/.test(lower)) && asksWhatIs) {
368
- const sources = sourcesFor("readme", "sharedState");
369
- return withSources([
370
- "Sessions are Waterbrother’s persisted work threads.",
371
- "They store cwd, runtime state, messages, and can be resumed locally or through Telegram-linked remote control."
372
- ], sources);
551
+ if (asksWhatIs || matchingCommands.length || matchingCapabilities.length) {
552
+ const subject = asksWhatIs
553
+ ? `Local Waterbrother context for: ${input}`
554
+ : "Local Waterbrother context";
555
+ return renderConceptAnswer({
556
+ subject,
557
+ capability: matchingCapabilities[0] || null,
558
+ commands: matchingCommands,
559
+ roomState,
560
+ extra: manifest.runtime?.provider || manifest.runtime?.model
561
+ ? [`Active runtime: ${manifest.runtime.provider || "unknown"}/${manifest.runtime.model || "unknown"}.`]
562
+ : []
563
+ });
373
564
  }
374
565
 
375
566
  return null;