@tritard/waterbrother 0.16.32 → 0.16.34

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
@@ -273,6 +273,19 @@ Shared project foundation is now live:
273
273
  - shared project metadata lives in `.waterbrother/shared.json`
274
274
  - human collaboration notes live in `ROUNDTABLE.md`
275
275
 
276
+ Waterbrother self-awareness is now live:
277
+ - `waterbrother about`
278
+ - `waterbrother capabilities`
279
+ - `waterbrother project-map`
280
+ - `waterbrother state`
281
+ - interactive:
282
+ - `/about`
283
+ - `/capabilities`
284
+ - `/project-map`
285
+ - `/state`
286
+ - Waterbrother now writes `.waterbrother/self-awareness.json`
287
+ - local questions like `what is roundtable?` are answered from repo state before generic model fallback
288
+
276
289
  Current Telegram behavior:
277
290
  - if the TUI is open, Telegram prompts are injected into the live TUI session and the work is visible in the terminal
278
291
  - if no live TUI session is attached, Telegram falls back to a remote run with `approval=never`
@@ -283,6 +296,8 @@ Current Telegram behavior:
283
296
  - Telegram now supports remote workspace control with `/cwd`, `/use <path>`, `/desktop`, and `/new-project <name>`
284
297
  - 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
285
298
  - `/room` now includes pending invite count plus task ownership summaries
299
+ - local `/status` now includes `sharedRoom` with pending invites, task ownership summary, and recent task activity
300
+ - Telegram now posts a Roundtable ownership notice when shared task ownership changes via assign/claim/move
286
301
  - shared Telegram execution only runs when the shared room is in `execute` mode
287
302
  - room administration is owner-only, and only owners/editors can hold the operator lock
288
303
  - `/room` status now shows the active executor surface plus provider/model/runtime identity
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.32",
3
+ "version": "0.16.34",
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/agent.js CHANGED
@@ -33,6 +33,10 @@ When you use tools:
33
33
  - cite the specific files or directories inspected
34
34
  - prefer concise subsystem summaries over long essays
35
35
  - do not offer a follow-up options menu unless asked
36
+ - For questions about Waterbrother itself, Roundtable, Telegram integration, or the current local repo:
37
+ - answer from local project state, project memory, and repo files first
38
+ - do not ask generic clarification questions if the answer is already present locally
39
+ - if you read local files to answer, name the specific files you used
36
40
  - For simple file or folder listing requests:
37
41
  - return a direct literal list unless the user asked for categorization or commentary
38
42
  - keep the response compact and avoid editorial summaries
package/src/cli.js CHANGED
@@ -57,6 +57,16 @@ import { runQualityChecks, formatQualityFindings, buildQualityFixPrompt } from "
57
57
  import { scanForInitiatives, formatInitiatives, buildInitiativeFixPrompt } from "./initiative.js";
58
58
  import { formatPlanForDisplay } from "./planner.js";
59
59
  import { parseCharterFromGoal, runExperimentLoop, formatExperimentSummary, gitReturnToBranch } from "./experiment.js";
60
+ import {
61
+ buildSelfAwarenessManifest,
62
+ buildSelfAwarenessMemoryBlock,
63
+ formatAboutWaterbrother,
64
+ formatCapabilitiesSummary,
65
+ formatProjectMap,
66
+ formatSelfState,
67
+ resolveLocalConceptQuestion,
68
+ saveSelfAwarenessManifest
69
+ } from "./self-awareness.js";
60
70
  import {
61
71
  addSharedTask,
62
72
  acceptSharedInvite,
@@ -100,6 +110,10 @@ const INTERACTIVE_COMMANDS = [
100
110
  { name: "/exit", description: "Exit interactive mode" },
101
111
  { name: "/quit", description: "Exit interactive mode" },
102
112
  { name: "/status", description: "Show runtime state" },
113
+ { name: "/about", description: "Show Waterbrother self-description from local state" },
114
+ { name: "/capabilities", description: "Show Waterbrother capabilities from local state" },
115
+ { name: "/project-map", description: "Show repo/codebase map from local state" },
116
+ { name: "/state", description: "Show full Waterbrother self-awareness state" },
103
117
  { name: "/whoami", description: "Show current operator identity and behavior" },
104
118
  { name: "/session", description: "Show current session metadata" },
105
119
  { name: "/sessions", description: "List recent sessions" },
@@ -317,6 +331,10 @@ Usage:
317
331
  waterbrother room mode <chat|plan|execute>
318
332
  waterbrother room claim
319
333
  waterbrother room release
334
+ waterbrother about
335
+ waterbrother capabilities
336
+ waterbrother project-map
337
+ waterbrother state
320
338
  waterbrother mcp list
321
339
  waterbrother commit [--push]
322
340
  waterbrother pr [--branch=<name>]
@@ -1875,6 +1893,18 @@ async function readProjectMemory(cwd) {
1875
1893
  };
1876
1894
  }
1877
1895
 
1896
+ async function refreshSelfAwareness(context, currentSession = null) {
1897
+ const manifest = await buildSelfAwarenessManifest({
1898
+ cwd: context.cwd,
1899
+ runtime: context.runtime,
1900
+ currentSession
1901
+ });
1902
+ context.runtime.selfAwareness = manifest;
1903
+ context.runtime.selfAwarenessBlock = buildSelfAwarenessMemoryBlock(manifest);
1904
+ await saveSelfAwarenessManifest(context.cwd, manifest).catch(() => {});
1905
+ return manifest;
1906
+ }
1907
+
1878
1908
  async function initProjectMemory(cwd) {
1879
1909
  const memoryPath = getProjectMemoryPath(cwd);
1880
1910
  try {
@@ -2652,10 +2682,23 @@ function buildRoomStatusPayload(project, runtime = null, currentSession = null)
2652
2682
  const assignee = String(task?.assignedTo || "").trim() || "unassigned";
2653
2683
  taskSummary.byAssignee[assignee] = (taskSummary.byAssignee[assignee] || 0) + 1;
2654
2684
  }
2685
+ const recentActivity = tasks
2686
+ .flatMap((task) => (Array.isArray(task.history) ? task.history.map((entry) => ({
2687
+ taskId: task.id,
2688
+ taskText: task.text,
2689
+ type: entry.type,
2690
+ text: entry.text,
2691
+ actorId: entry.actorId || "",
2692
+ actorName: entry.actorName || "",
2693
+ createdAt: entry.createdAt || ""
2694
+ })) : []))
2695
+ .sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)))
2696
+ .slice(0, 5);
2655
2697
  return {
2656
2698
  ...project,
2657
2699
  pendingInviteCount: Array.isArray(project.pendingInvites) ? project.pendingInvites.length : 0,
2658
2700
  taskSummary,
2701
+ recentActivity,
2659
2702
  executor: {
2660
2703
  surface: "local-tui",
2661
2704
  roomRuntimeProfile: project.runtimeProfile || "",
@@ -3830,6 +3873,54 @@ async function runProjectCommand(positional, { cwd = process.cwd(), asJson = fal
3830
3873
  throw new Error("Usage: waterbrother project share|unshare");
3831
3874
  }
3832
3875
 
3876
+ async function runSelfAwarenessCommand(positional, { cwd = process.cwd(), asJson = false, runtime = null, currentSession = null } = {}) {
3877
+ const sub = String(positional[0] || "state").trim().toLowerCase();
3878
+ const manifest = await buildSelfAwarenessManifest({ cwd, runtime: runtime || {}, currentSession });
3879
+ await saveSelfAwarenessManifest(cwd, manifest).catch(() => {});
3880
+
3881
+ if (sub === "about") {
3882
+ const payload = { ok: true, about: formatAboutWaterbrother(manifest), manifest };
3883
+ if (asJson) {
3884
+ printData(payload, true);
3885
+ return;
3886
+ }
3887
+ console.log(payload.about);
3888
+ return;
3889
+ }
3890
+
3891
+ if (sub === "capabilities") {
3892
+ const payload = { ok: true, manifest };
3893
+ if (asJson) {
3894
+ printData(payload, true);
3895
+ return;
3896
+ }
3897
+ console.log(formatCapabilitiesSummary(manifest));
3898
+ return;
3899
+ }
3900
+
3901
+ if (sub === "project-map") {
3902
+ const payload = { ok: true, manifest };
3903
+ if (asJson) {
3904
+ printData(payload, true);
3905
+ return;
3906
+ }
3907
+ console.log(formatProjectMap(manifest));
3908
+ return;
3909
+ }
3910
+
3911
+ if (sub === "state") {
3912
+ const payload = { ok: true, manifest };
3913
+ if (asJson) {
3914
+ printData(payload, true);
3915
+ return;
3916
+ }
3917
+ console.log(formatSelfState(manifest));
3918
+ return;
3919
+ }
3920
+
3921
+ throw new Error("Usage: waterbrother about|capabilities|project-map|state");
3922
+ }
3923
+
3833
3924
  async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false, runtime = null, currentSession = null, agent = null, context = null } = {}) {
3834
3925
  const sub = String(positional[1] || "status").trim().toLowerCase();
3835
3926
  const operator = getLocalOperatorIdentity();
@@ -5174,6 +5265,23 @@ async function runTextTurnInteractive({
5174
5265
  spinnerLabel = "thinking...",
5175
5266
  extraExecutionContext = null
5176
5267
  }) {
5268
+ await refreshSelfAwareness(context, currentSession).catch(() => {});
5269
+ const refreshedMemoryParts = [
5270
+ context.runtime.selfAwarenessBlock || "",
5271
+ context.runtime.projectMemory?.promptText || "",
5272
+ context.runtime.episodicMemory || "",
5273
+ context.runtime.product ? buildProductContext(context.runtime.product) : ""
5274
+ ].filter(Boolean);
5275
+ if (refreshedMemoryParts.length > 0) {
5276
+ agent.setMemory(refreshedMemoryParts.join("\n\n"));
5277
+ }
5278
+ const localConceptAnswer = resolveLocalConceptQuestion(promptText, context.runtime.selfAwareness);
5279
+ if (localConceptAnswer) {
5280
+ printAssistantOutput(localConceptAnswer);
5281
+ await saveCurrentSession(currentSession, agent);
5282
+ return true;
5283
+ }
5284
+
5177
5285
  const readOnlyRoots = extractExplicitReadOnlyRoots(promptText, context.cwd);
5178
5286
  const writeRoots = extractExplicitWriteRoots(promptText, context.cwd);
5179
5287
  const effectivePromptText = maybeRewriteExplicitInspectionPrompt(promptText, readOnlyRoots);
@@ -6449,7 +6557,9 @@ async function promptLoop(agent, session, context) {
6449
6557
  } catch {}
6450
6558
 
6451
6559
  // Combine all memory sources
6560
+ await refreshSelfAwareness(context, currentSession).catch(() => {});
6452
6561
  const memoryParts = [
6562
+ context.runtime.selfAwarenessBlock || "",
6453
6563
  context.runtime.projectMemory?.promptText || "",
6454
6564
  context.runtime.episodicMemory || "",
6455
6565
  context.runtime.product ? buildProductContext(context.runtime.product) : ""
@@ -7557,7 +7667,8 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7557
7667
  sessionApprovals: approvals,
7558
7668
  sessionApprovalsSummary: formatSessionApprovalsSummary(approvals),
7559
7669
  messageCount: agent.getSessionMessages().length,
7560
- mcp: mcpStatus
7670
+ mcp: mcpStatus,
7671
+ sharedRoom: await loadSharedProject(context.cwd).then((project) => buildRoomStatusPayload(project, context.runtime, currentSession)).catch(() => ({ enabled: false }))
7561
7672
  },
7562
7673
  null,
7563
7674
  2
@@ -7566,6 +7677,42 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7566
7677
  continue;
7567
7678
  }
7568
7679
 
7680
+ if (line === "/about") {
7681
+ try {
7682
+ await runSelfAwarenessCommand(["about"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession });
7683
+ } catch (error) {
7684
+ console.log(`about failed: ${error instanceof Error ? error.message : String(error)}`);
7685
+ }
7686
+ continue;
7687
+ }
7688
+
7689
+ if (line === "/capabilities") {
7690
+ try {
7691
+ await runSelfAwarenessCommand(["capabilities"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession });
7692
+ } catch (error) {
7693
+ console.log(`capabilities failed: ${error instanceof Error ? error.message : String(error)}`);
7694
+ }
7695
+ continue;
7696
+ }
7697
+
7698
+ if (line === "/project-map") {
7699
+ try {
7700
+ await runSelfAwarenessCommand(["project-map"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession });
7701
+ } catch (error) {
7702
+ console.log(`project-map failed: ${error instanceof Error ? error.message : String(error)}`);
7703
+ }
7704
+ continue;
7705
+ }
7706
+
7707
+ if (line === "/state") {
7708
+ try {
7709
+ await runSelfAwarenessCommand(["state"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession });
7710
+ } catch (error) {
7711
+ console.log(`state failed: ${error instanceof Error ? error.message : String(error)}`);
7712
+ }
7713
+ continue;
7714
+ }
7715
+
7569
7716
  if (line === "/session") {
7570
7717
  const mcpStatus = agent.toolRuntime.getMcpStatus();
7571
7718
  const approvals = agent.toolRuntime.getSessionApprovals ? agent.toolRuntime.getSessionApprovals() : { allowShellCommands: [], allowTools: [] };
@@ -9989,6 +10136,61 @@ export async function runCli(argv) {
9989
10136
  let { userConfig, config } = await loadConfigLayers(startupCwd);
9990
10137
  let runtime = resolveRuntimeConfig(config, configOverrides);
9991
10138
  runtime.autoCompactThreshold = normalizeAutoCompactThreshold(runtime.autoCompactThreshold);
10139
+ const knownCommands = new Set([
10140
+ "chat",
10141
+ "exec",
10142
+ "review",
10143
+ "resume",
10144
+ "config",
10145
+ "sessions",
10146
+ "models",
10147
+ "runtime-profiles",
10148
+ "channels",
10149
+ "project",
10150
+ "about",
10151
+ "capabilities",
10152
+ "project-map",
10153
+ "state",
10154
+ "room",
10155
+ "gateway",
10156
+ "mcp",
10157
+ "onboarding",
10158
+ "vision",
10159
+ "update",
10160
+ "doctor",
10161
+ "commit",
10162
+ "pr"
10163
+ ]);
10164
+ const canReadPromptFromStdin =
10165
+ Boolean(flags.pipe) ||
10166
+ command === "chat" ||
10167
+ command === "exec" ||
10168
+ command === "review" ||
10169
+ command === "resume" ||
10170
+ (positional.length > 0 && !knownCommands.has(command));
10171
+ let earlyPrompt = "";
10172
+ if (flags.prompt) {
10173
+ earlyPrompt = String(flags.prompt).trim();
10174
+ } else if (command === "chat" || command === "exec" || command === "review") {
10175
+ earlyPrompt = positional.slice(1).join(" ").trim();
10176
+ } else if (positional.length > 0 && !knownCommands.has(command)) {
10177
+ earlyPrompt = positional.join(" ").trim();
10178
+ } else if (!process.stdin.isTTY && canReadPromptFromStdin) {
10179
+ earlyPrompt = (await readStdinIfNeeded()) || "";
10180
+ }
10181
+ if (earlyPrompt) {
10182
+ const earlyManifest = await buildSelfAwarenessManifest({ cwd: startupCwd, runtime });
10183
+ const earlyLocalConceptAnswer = resolveLocalConceptQuestion(earlyPrompt, earlyManifest);
10184
+ if (earlyLocalConceptAnswer) {
10185
+ await saveSelfAwarenessManifest(startupCwd, earlyManifest).catch(() => {});
10186
+ if (asJson) {
10187
+ printData({ content: earlyLocalConceptAnswer, usage: null, sessionId: null, model: runtime.model || "" }, true);
10188
+ } else {
10189
+ console.log(earlyLocalConceptAnswer);
10190
+ }
10191
+ return;
10192
+ }
10193
+ }
9992
10194
 
9993
10195
  if (command === "doctor") {
9994
10196
  await runDoctorCommand({ runtime, cwd: startupCwd, asJson });
@@ -10015,6 +10217,11 @@ export async function runCli(argv) {
10015
10217
  return;
10016
10218
  }
10017
10219
 
10220
+ if (command === "about" || command === "capabilities" || command === "project-map" || command === "state") {
10221
+ await runSelfAwarenessCommand([command], { cwd: startupCwd, asJson, runtime });
10222
+ return;
10223
+ }
10224
+
10018
10225
  if (command === "room") {
10019
10226
  await runRoomCommand(positional, { cwd: startupCwd, asJson, runtime });
10020
10227
  return;
@@ -10173,6 +10380,16 @@ export async function runCli(argv) {
10173
10380
  });
10174
10381
 
10175
10382
  agent.hydrateFromSessionMessages(session.messages || []);
10383
+ const initialSelfAwareness = await buildSelfAwarenessManifest({ cwd, runtime, currentSession: session });
10384
+ runtime.selfAwareness = initialSelfAwareness;
10385
+ runtime.selfAwarenessBlock = buildSelfAwarenessMemoryBlock(initialSelfAwareness);
10386
+ await saveSelfAwarenessManifest(cwd, initialSelfAwareness).catch(() => {});
10387
+ agent.setMemory(
10388
+ [
10389
+ runtime.selfAwarenessBlock || "",
10390
+ runtime.projectMemory?.promptText || ""
10391
+ ].filter(Boolean).join("\n\n")
10392
+ );
10176
10393
 
10177
10394
  if (flags["print-session-id"]) {
10178
10395
  console.log(session.id);
@@ -10182,33 +10399,7 @@ export async function runCli(argv) {
10182
10399
  const pipeMode = Boolean(flags.pipe);
10183
10400
  const outputFormat = String(flags["output-format"] || "text").toLowerCase();
10184
10401
  const machineReadableOutput = outputFormat === "json" || outputFormat === "stream-json";
10185
- const knownCommands = new Set([
10186
- "chat",
10187
- "exec",
10188
- "review",
10189
- "resume",
10190
- "config",
10191
- "sessions",
10192
- "models",
10193
- "channels",
10194
- "gateway",
10195
- "mcp",
10196
- "onboarding",
10197
- "vision",
10198
- "update",
10199
- "doctor",
10200
- "commit",
10201
- "pr"
10202
- ]);
10203
- const canReadPromptFromStdin =
10204
- pipeMode ||
10205
- command === "chat" ||
10206
- command === "exec" ||
10207
- command === "review" ||
10208
- command === "resume" ||
10209
- (positional.length > 0 && !knownCommands.has(command));
10210
-
10211
- let oneShotPrompt = "";
10402
+ let oneShotPrompt = earlyPrompt || "";
10212
10403
  if (promptFlag) {
10213
10404
  oneShotPrompt = promptFlag;
10214
10405
  } else if (command === "chat" || command === "exec" || command === "review") {
@@ -10219,9 +10410,11 @@ export async function runCli(argv) {
10219
10410
  } else if (positional.length > 0 && !knownCommands.has(command)) {
10220
10411
  oneShotPrompt = positional.join(" ").trim();
10221
10412
  } else if (!process.stdin.isTTY && canReadPromptFromStdin) {
10222
- const stdinPrompt = await readStdinIfNeeded();
10223
- if (stdinPrompt) {
10224
- oneShotPrompt = stdinPrompt;
10413
+ if (!oneShotPrompt) {
10414
+ const stdinPrompt = await readStdinIfNeeded();
10415
+ if (stdinPrompt) {
10416
+ oneShotPrompt = stdinPrompt;
10417
+ }
10225
10418
  }
10226
10419
  }
10227
10420
 
@@ -10260,6 +10453,15 @@ export async function runCli(argv) {
10260
10453
  const exactShellCommand = extractExactShellCommand(oneShotPrompt);
10261
10454
 
10262
10455
  if (oneShotPrompt) {
10456
+ const localConceptAnswer = resolveLocalConceptQuestion(oneShotPrompt, runtime.selfAwareness);
10457
+ if (localConceptAnswer) {
10458
+ if (outputFormat === "json") {
10459
+ console.log(JSON.stringify({ content: localConceptAnswer, usage: null, sessionId: session.id, model: agent.getModel() }, null, 2));
10460
+ } else {
10461
+ console.log(sanitizeTerminalText(localConceptAnswer));
10462
+ }
10463
+ return;
10464
+ }
10263
10465
  if (exactShellCommand) {
10264
10466
  await runExactShellTurn({
10265
10467
  agent,
package/src/gateway.js CHANGED
@@ -411,6 +411,23 @@ function formatTelegramTaskHistoryMarkup(result) {
411
411
  return lines.join("\n");
412
412
  }
413
413
 
414
+ function buildTaskOwnershipNotice(task, { action = "updated", previousAssignee = "", previousState = "" } = {}) {
415
+ if (!task) return "";
416
+ const bits = [
417
+ "<b>Roundtable update</b>",
418
+ `<code>${escapeTelegramHtml(task.id || "")}</code> ${escapeTelegramHtml(task.text || "")}`
419
+ ];
420
+ if (action === "assign") {
421
+ bits.push(`owner: <code>${escapeTelegramHtml(previousAssignee || "unassigned")}</code> → <code>${escapeTelegramHtml(task.assignedTo || "unassigned")}</code>`);
422
+ } else if (action === "claim") {
423
+ bits.push(`claimed by <code>${escapeTelegramHtml(task.assignedTo || "unassigned")}</code>`);
424
+ } else if (action === "move") {
425
+ bits.push(`state: <code>${escapeTelegramHtml(previousState || "open")}</code> → <code>${escapeTelegramHtml(task.state || "open")}</code>`);
426
+ if (task.assignedTo) bits.push(`owner: <code>${escapeTelegramHtml(task.assignedTo)}</code>`);
427
+ }
428
+ return bits.join("\n");
429
+ }
430
+
414
431
  function classifyTelegramGroupIntent(text = "") {
415
432
  const normalized = String(text || "").trim();
416
433
  const lower = normalized.toLowerCase();
@@ -1395,8 +1412,11 @@ class TelegramGateway {
1395
1412
  return;
1396
1413
  }
1397
1414
  try {
1415
+ const before = (project?.tasks || []).find((task) => task.id === taskId) || null;
1398
1416
  const result = await moveSharedTask(session.cwd || this.cwd, taskId, state, { actorId: userId });
1399
1417
  await this.sendMessage(message.chat.id, `Moved shared task <code>${escapeTelegramHtml(result.task.id)}</code> to <code>${escapeTelegramHtml(result.task.state)}</code>`, message.message_id);
1418
+ const notice = buildTaskOwnershipNotice(result.task, { action: "move", previousAssignee: before?.assignedTo || "", previousState: before?.state || "" });
1419
+ if (notice) await this.sendMessage(message.chat.id, notice, null, { parseMode: "HTML" });
1400
1420
  } catch (error) {
1401
1421
  await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
1402
1422
  }
@@ -1415,8 +1435,11 @@ class TelegramGateway {
1415
1435
  return;
1416
1436
  }
1417
1437
  try {
1438
+ const before = (project?.tasks || []).find((task) => task.id === taskId) || null;
1418
1439
  const result = await assignSharedTask(session.cwd || this.cwd, taskId, memberId, { actorId: userId });
1419
1440
  await this.sendMessage(message.chat.id, `Assigned shared task <code>${escapeTelegramHtml(result.task.id)}</code> to <code>${escapeTelegramHtml(memberId)}</code>`, message.message_id);
1441
+ const notice = buildTaskOwnershipNotice(result.task, { action: "assign", previousAssignee: before?.assignedTo || "" });
1442
+ if (notice) await this.sendMessage(message.chat.id, notice, null, { parseMode: "HTML" });
1420
1443
  } catch (error) {
1421
1444
  await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
1422
1445
  }
@@ -1482,6 +1505,8 @@ class TelegramGateway {
1482
1505
  try {
1483
1506
  const result = await claimSharedTask(session.cwd || this.cwd, taskId, { actorId: userId });
1484
1507
  await this.sendMessage(message.chat.id, `Claimed shared task <code>${escapeTelegramHtml(result.task.id)}</code>`, message.message_id);
1508
+ const notice = buildTaskOwnershipNotice(result.task, { action: "claim" });
1509
+ if (notice) await this.sendMessage(message.chat.id, notice, null, { parseMode: "HTML" });
1485
1510
  } catch (error) {
1486
1511
  await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
1487
1512
  }
@@ -0,0 +1,291 @@
1
+ import { execFile as execFileCb } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { loadProduct } from "./product.js";
6
+ import { loadSharedProject } from "./shared-project.js";
7
+
8
+ const execFile = promisify(execFileCb);
9
+ const SELF_AWARENESS_FILE = path.join(".waterbrother", "self-awareness.json");
10
+
11
+ async function readJson(filePath) {
12
+ try {
13
+ return JSON.parse(await fs.readFile(filePath, "utf8"));
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ async function readText(filePath) {
20
+ try {
21
+ return await fs.readFile(filePath, "utf8");
22
+ } catch {
23
+ return "";
24
+ }
25
+ }
26
+
27
+ async function exists(filePath) {
28
+ try {
29
+ await fs.access(filePath);
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ function firstHeading(markdown = "") {
37
+ const match = String(markdown || "").match(/^#\s+(.+)$/m);
38
+ return match ? String(match[1] || "").trim() : "";
39
+ }
40
+
41
+ async function detectGit(cwd) {
42
+ try {
43
+ const { stdout: root } = await execFile("git", ["rev-parse", "--show-toplevel"], { cwd, timeout: 10000 });
44
+ const { stdout: branch } = await execFile("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd, timeout: 10000 });
45
+ const { stdout: status } = await execFile("git", ["status", "--short"], { cwd, timeout: 10000, maxBuffer: 1024 * 1024 });
46
+ return {
47
+ root: String(root || "").trim(),
48
+ branch: String(branch || "").trim(),
49
+ dirty: Boolean(String(status || "").trim())
50
+ };
51
+ } catch {
52
+ return { root: "", branch: "", dirty: false };
53
+ }
54
+ }
55
+
56
+ async function listTopLevelEntries(cwd) {
57
+ try {
58
+ const entries = await fs.readdir(cwd, { withFileTypes: true });
59
+ return entries
60
+ .filter((entry) => !entry.name.startsWith("."))
61
+ .slice(0, 14)
62
+ .map((entry) => ({
63
+ name: entry.name,
64
+ kind: entry.isDirectory() ? "dir" : "file"
65
+ }));
66
+ } catch {
67
+ return [];
68
+ }
69
+ }
70
+
71
+ function detectCapabilities(cwd, sharedProject, docsPresence = {}) {
72
+ const capabilities = [
73
+ "BYOM runtime profiles",
74
+ "provider/model switching",
75
+ "interactive terminal coding",
76
+ "session persistence",
77
+ "Telegram messaging gateway"
78
+ ];
79
+ if (sharedProject?.enabled) capabilities.push("Roundtable shared projects");
80
+ if (docsPresence.roundtable) capabilities.push("Roundtable collaboration docs");
81
+ if (docsPresence.telegram) capabilities.push("Telegram integration docs");
82
+ return capabilities;
83
+ }
84
+
85
+ function buildProjectMapSummary({ entries = [], packageJson = null, sharedProject = null, product = null }) {
86
+ const keyFiles = [];
87
+ if (packageJson?.name) keyFiles.push("package.json");
88
+ if (entries.some((entry) => entry.name === "README.md")) keyFiles.push("README.md");
89
+ if (entries.some((entry) => entry.name === "WATERBROTHER.md")) keyFiles.push("WATERBROTHER.md");
90
+ if (entries.some((entry) => entry.name === "ROUNDTABLE.md")) keyFiles.push("ROUNDTABLE.md");
91
+ if (entries.some((entry) => entry.name === "src")) keyFiles.push("src/");
92
+ if (entries.some((entry) => entry.name === "assets")) keyFiles.push("assets/");
93
+ return {
94
+ keyFiles,
95
+ shared: sharedProject?.enabled === true,
96
+ product: product?.name || "",
97
+ topLevel: entries
98
+ };
99
+ }
100
+
101
+ export async function buildSelfAwarenessManifest({ cwd, runtime = {}, currentSession = null } = {}) {
102
+ const [packageJson, readme, memoryText, roundtableText, sharedProject, product, git, entries] = await Promise.all([
103
+ readJson(path.join(cwd, "package.json")),
104
+ readText(path.join(cwd, "README.md")),
105
+ readText(path.join(cwd, "WATERBROTHER.md")),
106
+ readText(path.join(cwd, "ROUNDTABLE.md")),
107
+ loadSharedProject(cwd),
108
+ loadProduct(cwd),
109
+ detectGit(cwd),
110
+ listTopLevelEntries(cwd)
111
+ ]);
112
+
113
+ const docsPresence = {
114
+ integrations: await exists(path.join(cwd, "integrations", "index.html")),
115
+ roundtable: await exists(path.join(cwd, "roundtable", "index.html")),
116
+ telegram: await exists(path.join(cwd, "channels", "telegram", "index.html"))
117
+ };
118
+
119
+ const projectMap = buildProjectMapSummary({ entries, packageJson, sharedProject, product });
120
+ const manifest = {
121
+ version: 1,
122
+ generatedAt: new Date().toISOString(),
123
+ identity: {
124
+ name: "Waterbrother",
125
+ description: "Bring-your-own-model local coding CLI with sessions, approvals, messaging, and shared project collaboration.",
126
+ cwd,
127
+ repoName: packageJson?.name || path.basename(cwd),
128
+ readmeTitle: firstHeading(readme) || "",
129
+ git
130
+ },
131
+ runtime: {
132
+ provider: runtime.provider || "",
133
+ model: runtime.model || "",
134
+ designModel: runtime.designModel || "",
135
+ agentProfile: runtime.agentProfile || "",
136
+ runtimeProfile: currentSession?.runtimeProfile || "",
137
+ toolsEnabled: runtime.enableTools !== false,
138
+ traceMode: runtime.traceMode || "",
139
+ receiptMode: runtime.receiptMode || ""
140
+ },
141
+ features: {
142
+ capabilities: detectCapabilities(cwd, sharedProject, docsPresence),
143
+ docsPresence,
144
+ channels: ["telegram", "discord", "signal"],
145
+ sharedProjectEnabled: sharedProject?.enabled === true
146
+ },
147
+ project: {
148
+ packageName: packageJson?.name || "",
149
+ packageVersion: packageJson?.version || "",
150
+ packageType: packageJson?.type || "",
151
+ scripts: Object.keys(packageJson?.scripts || {}).slice(0, 10),
152
+ readmeTitle: firstHeading(readme) || "",
153
+ projectMemoryTitle: firstHeading(memoryText) || "",
154
+ roundtableTitle: firstHeading(roundtableText) || "",
155
+ projectMap
156
+ },
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 },
168
+ product: product
169
+ ? {
170
+ name: product.name || "",
171
+ type: product.type || "",
172
+ surfaceCount: Array.isArray(product.surfaces) ? product.surfaces.length : 0
173
+ }
174
+ : 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" : ""
182
+ }
183
+ };
184
+
185
+ return manifest;
186
+ }
187
+
188
+ export async function saveSelfAwarenessManifest(cwd, manifest) {
189
+ const target = path.join(cwd, SELF_AWARENESS_FILE);
190
+ await fs.mkdir(path.dirname(target), { recursive: true });
191
+ await fs.writeFile(target, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
192
+ return target;
193
+ }
194
+
195
+ export function buildSelfAwarenessMemoryBlock(manifest = {}) {
196
+ const lines = [
197
+ "Waterbrother self-awareness",
198
+ `- identity: ${manifest.identity?.name || "Waterbrother"}`,
199
+ `- description: ${manifest.identity?.description || ""}`,
200
+ `- repo: ${manifest.identity?.repoName || ""}`,
201
+ `- cwd: ${manifest.identity?.cwd || ""}`,
202
+ `- git branch: ${manifest.identity?.git?.branch || "none"}`,
203
+ `- runtime: ${manifest.runtime?.provider || "unknown"}/${manifest.runtime?.model || "unknown"}`,
204
+ `- shared room: ${manifest.sharedRoom?.enabled ? `${manifest.sharedRoom.roomMode || "chat"} mode` : "off"}`,
205
+ `- capabilities: ${(manifest.features?.capabilities || []).join(", ")}`
206
+ ];
207
+ return lines.filter(Boolean).join("\n");
208
+ }
209
+
210
+ export function formatCapabilitiesSummary(manifest = {}) {
211
+ return JSON.stringify({
212
+ identity: manifest.identity,
213
+ capabilities: manifest.features?.capabilities || [],
214
+ channels: manifest.features?.channels || [],
215
+ sharedRoomEnabled: manifest.sharedRoom?.enabled === true
216
+ }, null, 2);
217
+ }
218
+
219
+ export function formatProjectMap(manifest = {}) {
220
+ return JSON.stringify({
221
+ repo: manifest.identity?.repoName || "",
222
+ cwd: manifest.identity?.cwd || "",
223
+ git: manifest.identity?.git || {},
224
+ packageName: manifest.project?.packageName || "",
225
+ scripts: manifest.project?.scripts || [],
226
+ topLevel: manifest.project?.projectMap?.topLevel || [],
227
+ keyFiles: manifest.project?.projectMap?.keyFiles || [],
228
+ sharedProject: manifest.sharedRoom || { enabled: false },
229
+ product: manifest.product || null
230
+ }, null, 2);
231
+ }
232
+
233
+ export function formatSelfState(manifest = {}) {
234
+ return JSON.stringify(manifest, null, 2);
235
+ }
236
+
237
+ export function formatAboutWaterbrother(manifest = {}) {
238
+ const sources = [
239
+ manifest.sourceHints?.readme,
240
+ manifest.sourceHints?.roundtableDocs,
241
+ manifest.sourceHints?.telegramDocs,
242
+ manifest.sourceHints?.sharedState
243
+ ].filter(Boolean);
244
+ return [
245
+ `Waterbrother is ${manifest.identity?.description || "a local coding CLI."}`,
246
+ `repo: ${manifest.identity?.repoName || "-"}`,
247
+ `cwd: ${manifest.identity?.cwd || "-"}`,
248
+ `capabilities: ${(manifest.features?.capabilities || []).join(", ") || "-"}`,
249
+ sources.length ? `sources: ${sources.join(", ")}` : ""
250
+ ].filter(Boolean).join("\n");
251
+ }
252
+
253
+ export function resolveLocalConceptQuestion(text = "", manifest = {}) {
254
+ const input = String(text || "").trim();
255
+ const lower = input.toLowerCase();
256
+ if (!lower) return null;
257
+
258
+ const mentionsRoundtable = /\bround[\s-]?table\b/.test(lower);
259
+ const asksWhatIs = /^(what('| i)?s|what is|tell me about|explain)\b/.test(lower) || /\bwhat is\b/.test(lower);
260
+ if (mentionsRoundtable && asksWhatIs) {
261
+ const sources = [manifest.sourceHints?.roundtableDocs, manifest.sourceHints?.sharedState, manifest.sourceHints?.roundtable].filter(Boolean);
262
+ return [
263
+ "Roundtable is Waterbrother’s shared-project collaboration model.",
264
+ "It ties one project to one shared room with explicit room modes, member roles, operator lock, tasks, invites, and shared runtime selection.",
265
+ `sources: ${sources.join(", ")}`
266
+ ].join("\n");
267
+ }
268
+
269
+ if ((/\bwhat can you do\b/.test(lower) || /\bcapabilities\b/.test(lower) || /\bwhat do you support\b/.test(lower))) {
270
+ return formatCapabilitiesSummary(manifest);
271
+ }
272
+
273
+ if ((/\bwhat is this project\b/.test(lower) || /\bwhat repo is this\b/.test(lower) || /\bwhat codebase is this\b/.test(lower))) {
274
+ return formatProjectMap(manifest);
275
+ }
276
+
277
+ if (/\bwhat is waterbrother\b/.test(lower) || /\bwho are you\b/.test(lower) || /\bwhat are you\b/.test(lower)) {
278
+ return formatAboutWaterbrother(manifest);
279
+ }
280
+
281
+ if (/\btelegram\b/.test(lower) && asksWhatIs) {
282
+ const sources = [manifest.sourceHints?.telegramDocs, manifest.sourceHints?.sharedState].filter(Boolean);
283
+ return [
284
+ "Telegram is Waterbrother’s first live messaging adapter.",
285
+ "It supports pairing, live TUI bridging, shared-room commands, task operations, and room execution gating.",
286
+ `sources: ${sources.join(", ")}`
287
+ ].join("\n");
288
+ }
289
+
290
+ return null;
291
+ }