@tritard/waterbrother 0.16.27 → 0.16.29
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 +4 -1
- package/package.json +1 -1
- package/src/cli.js +125 -12
- package/src/gateway.js +140 -7
- package/src/shared-project.js +116 -5
package/README.md
CHANGED
|
@@ -266,6 +266,7 @@ Shared project foundation is now live:
|
|
|
266
266
|
- inspect it with `waterbrother room status`
|
|
267
267
|
- control conversation vs execution with `waterbrother room mode chat|plan|execute`
|
|
268
268
|
- manage collaborators with `waterbrother room members`, `waterbrother room add`, and `waterbrother room remove`
|
|
269
|
+
- manage the shared backlog with `waterbrother room tasks`, `waterbrother room task add`, and `waterbrother room task move`
|
|
269
270
|
- claim or release the shared operator lock with `waterbrother room claim` and `waterbrother room release`
|
|
270
271
|
- shared project metadata lives in `.waterbrother/shared.json`
|
|
271
272
|
- human collaboration notes live in `ROUNDTABLE.md`
|
|
@@ -278,10 +279,12 @@ Current Telegram behavior:
|
|
|
278
279
|
- pending pairings are explicit and expire automatically after 12 hours unless approved
|
|
279
280
|
- paired Telegram users drive the same live session and permissions as the terminal operator when the TUI bridge is attached
|
|
280
281
|
- Telegram now supports remote workspace control with `/cwd`, `/use <path>`, `/desktop`, and `/new-project <name>`
|
|
281
|
-
- shared projects now support `/room`, `/members`, `/mode`, `/claim`, `/release`, `/invite`,
|
|
282
|
+
- shared projects now support `/room`, `/members`, `/tasks`, `/mode`, `/claim`, `/release`, `/invite`, `/remove-member`, and `/task ...` from Telegram
|
|
282
283
|
- shared Telegram execution only runs when the shared room is in `execute` mode
|
|
283
284
|
- room administration is owner-only, and only owners/editors can hold the operator lock
|
|
285
|
+
- `/room` status now shows the active executor surface plus provider/model/runtime identity
|
|
284
286
|
- in Telegram groups, Waterbrother only responds when directly targeted: slash commands, `@botname` mentions, or replies to a bot message
|
|
287
|
+
- in Telegram groups, directly targeted messages are now classified as chat, planning, or execution; only explicit execution requests run the live session
|
|
285
288
|
- pairing is now explicit: first DM creates a pending request, then approve locally with `waterbrother gateway pair telegram <user-id>`
|
|
286
289
|
|
|
287
290
|
## Release flow
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -58,13 +58,16 @@ import { scanForInitiatives, formatInitiatives, buildInitiativeFixPrompt } from
|
|
|
58
58
|
import { formatPlanForDisplay } from "./planner.js";
|
|
59
59
|
import { parseCharterFromGoal, runExperimentLoop, formatExperimentSummary, gitReturnToBranch } from "./experiment.js";
|
|
60
60
|
import {
|
|
61
|
+
addSharedTask,
|
|
61
62
|
claimSharedOperator,
|
|
62
63
|
disableSharedProject,
|
|
63
64
|
enableSharedProject,
|
|
64
65
|
formatSharedProjectStatus,
|
|
65
66
|
getSharedProjectPaths,
|
|
66
67
|
listSharedMembers,
|
|
68
|
+
listSharedTasks,
|
|
67
69
|
loadSharedProject,
|
|
70
|
+
moveSharedTask,
|
|
68
71
|
releaseSharedOperator,
|
|
69
72
|
removeSharedMember,
|
|
70
73
|
setSharedRoomMode,
|
|
@@ -154,6 +157,9 @@ const INTERACTIVE_COMMANDS = [
|
|
|
154
157
|
{ name: "/room members", description: "List shared-project members" },
|
|
155
158
|
{ name: "/room add <id> [owner|editor|observer]", description: "Add or update a shared-project member" },
|
|
156
159
|
{ name: "/room remove <id>", description: "Remove a shared-project member" },
|
|
160
|
+
{ name: "/room tasks", description: "List Roundtable tasks for the shared project" },
|
|
161
|
+
{ name: "/room task add <text>", description: "Add a Roundtable task" },
|
|
162
|
+
{ name: "/room task move <id> <open|active|blocked|done>", description: "Move a Roundtable task between states" },
|
|
157
163
|
{ name: "/room mode <chat|plan|execute>", description: "Set collaboration mode for the shared room" },
|
|
158
164
|
{ name: "/room claim", description: "Claim operator control for the shared room" },
|
|
159
165
|
{ name: "/room release", description: "Release operator control for the shared room" },
|
|
@@ -281,6 +287,9 @@ Usage:
|
|
|
281
287
|
waterbrother room members
|
|
282
288
|
waterbrother room add <member-id> [owner|editor|observer]
|
|
283
289
|
waterbrother room remove <member-id>
|
|
290
|
+
waterbrother room tasks
|
|
291
|
+
waterbrother room task add <text>
|
|
292
|
+
waterbrother room task move <id> <open|active|blocked|done>
|
|
284
293
|
waterbrother room mode <chat|plan|execute>
|
|
285
294
|
waterbrother room claim
|
|
286
295
|
waterbrother room release
|
|
@@ -2603,6 +2612,23 @@ function buildRuntimeStatusPayload(runtime, agent, currentSession = null) {
|
|
|
2603
2612
|
};
|
|
2604
2613
|
}
|
|
2605
2614
|
|
|
2615
|
+
function buildRoomStatusPayload(project, runtime = null, currentSession = null) {
|
|
2616
|
+
if (!project) {
|
|
2617
|
+
return { enabled: false };
|
|
2618
|
+
}
|
|
2619
|
+
return {
|
|
2620
|
+
...project,
|
|
2621
|
+
executor: {
|
|
2622
|
+
surface: "local-tui",
|
|
2623
|
+
runtimeProfile: currentSession?.runtimeProfile || runtime?.gateway?.defaultRuntimeProfile || "",
|
|
2624
|
+
provider: runtime?.provider || "unknown",
|
|
2625
|
+
model: runtime?.model || "unknown",
|
|
2626
|
+
designModel: runtime?.designModel || "unknown",
|
|
2627
|
+
agentProfile: runtime?.agentProfile || "coder"
|
|
2628
|
+
}
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2606
2632
|
async function applyRuntimeSelection({
|
|
2607
2633
|
config,
|
|
2608
2634
|
context,
|
|
@@ -3765,16 +3791,17 @@ async function runProjectCommand(positional, { cwd = process.cwd(), asJson = fal
|
|
|
3765
3791
|
throw new Error("Usage: waterbrother project share|unshare");
|
|
3766
3792
|
}
|
|
3767
3793
|
|
|
3768
|
-
async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false } = {}) {
|
|
3794
|
+
async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false, runtime = null, currentSession = null } = {}) {
|
|
3769
3795
|
const sub = String(positional[1] || "status").trim().toLowerCase();
|
|
3770
3796
|
const operator = getLocalOperatorIdentity();
|
|
3771
3797
|
if (sub === "status") {
|
|
3772
3798
|
const project = await loadSharedProject(cwd);
|
|
3799
|
+
const payload = buildRoomStatusPayload(project, runtime, currentSession);
|
|
3773
3800
|
if (asJson) {
|
|
3774
|
-
printData(
|
|
3801
|
+
printData(payload, true);
|
|
3775
3802
|
return;
|
|
3776
3803
|
}
|
|
3777
|
-
console.log(
|
|
3804
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3778
3805
|
return;
|
|
3779
3806
|
}
|
|
3780
3807
|
|
|
@@ -3794,6 +3821,54 @@ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false
|
|
|
3794
3821
|
return;
|
|
3795
3822
|
}
|
|
3796
3823
|
|
|
3824
|
+
if (sub === "tasks") {
|
|
3825
|
+
const tasks = await listSharedTasks(cwd);
|
|
3826
|
+
if (asJson) {
|
|
3827
|
+
printData({ ok: true, tasks }, true);
|
|
3828
|
+
return;
|
|
3829
|
+
}
|
|
3830
|
+
if (!tasks.length) {
|
|
3831
|
+
console.log("No shared-project tasks");
|
|
3832
|
+
return;
|
|
3833
|
+
}
|
|
3834
|
+
for (const task of tasks) {
|
|
3835
|
+
console.log(`${task.id}\t${task.state}\t${task.text}`);
|
|
3836
|
+
}
|
|
3837
|
+
return;
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
if (sub === "task") {
|
|
3841
|
+
const action = String(positional[2] || "").trim().toLowerCase();
|
|
3842
|
+
if (action === "add") {
|
|
3843
|
+
const text = String(positional.slice(3).join(" ") || "").trim();
|
|
3844
|
+
if (!text) {
|
|
3845
|
+
throw new Error("Usage: waterbrother room task add <text>");
|
|
3846
|
+
}
|
|
3847
|
+
const result = await addSharedTask(cwd, text, { actorId: operator.id });
|
|
3848
|
+
if (asJson) {
|
|
3849
|
+
printData({ ok: true, action: "task-add", task: result.task, project: result.project }, true);
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
3852
|
+
console.log(`Added shared task [${result.task.id}] ${result.task.text}`);
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
3855
|
+
if (action === "move") {
|
|
3856
|
+
const taskId = String(positional[3] || "").trim();
|
|
3857
|
+
const state = String(positional[4] || "").trim().toLowerCase();
|
|
3858
|
+
if (!taskId || !state) {
|
|
3859
|
+
throw new Error("Usage: waterbrother room task move <id> <open|active|blocked|done>");
|
|
3860
|
+
}
|
|
3861
|
+
const result = await moveSharedTask(cwd, taskId, state, { actorId: operator.id });
|
|
3862
|
+
if (asJson) {
|
|
3863
|
+
printData({ ok: true, action: "task-move", task: result.task, project: result.project }, true);
|
|
3864
|
+
return;
|
|
3865
|
+
}
|
|
3866
|
+
console.log(`Moved shared task [${result.task.id}] to ${result.task.state}`);
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3869
|
+
throw new Error("Usage: waterbrother room task add <text>|move <id> <open|active|blocked|done>");
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3797
3872
|
if (sub === "add") {
|
|
3798
3873
|
const memberId = String(positional[2] || "").trim();
|
|
3799
3874
|
const role = String(positional[3] || "editor").trim().toLowerCase();
|
|
@@ -3868,7 +3943,7 @@ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false
|
|
|
3868
3943
|
return;
|
|
3869
3944
|
}
|
|
3870
3945
|
|
|
3871
|
-
throw new Error("Usage: waterbrother room status|members|add <member-id> [owner|editor|observer] [display name]|remove <member-id>|mode <chat|plan|execute>|claim|release");
|
|
3946
|
+
throw new Error("Usage: waterbrother room status|members|add <member-id> [owner|editor|observer] [display name]|remove <member-id>|tasks|task add <text>|task move <id> <open|active|blocked|done>|mode <chat|plan|execute>|claim|release");
|
|
3872
3947
|
}
|
|
3873
3948
|
|
|
3874
3949
|
async function runMcpCommand(positional, runtime, { cwd, asJson = false } = {}) {
|
|
@@ -7689,7 +7764,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7689
7764
|
|
|
7690
7765
|
if (line === "/room") {
|
|
7691
7766
|
try {
|
|
7692
|
-
await runRoomCommand(["room", "status"], { cwd: context.cwd, asJson: false });
|
|
7767
|
+
await runRoomCommand(["room", "status"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession });
|
|
7693
7768
|
} catch (error) {
|
|
7694
7769
|
console.log(`room status failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7695
7770
|
}
|
|
@@ -7698,13 +7773,22 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7698
7773
|
|
|
7699
7774
|
if (line === "/room members") {
|
|
7700
7775
|
try {
|
|
7701
|
-
await runRoomCommand(["room", "members"], { cwd: context.cwd, asJson: false });
|
|
7776
|
+
await runRoomCommand(["room", "members"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession });
|
|
7702
7777
|
} catch (error) {
|
|
7703
7778
|
console.log(`room members failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7704
7779
|
}
|
|
7705
7780
|
continue;
|
|
7706
7781
|
}
|
|
7707
7782
|
|
|
7783
|
+
if (line === "/room tasks") {
|
|
7784
|
+
try {
|
|
7785
|
+
await runRoomCommand(["room", "tasks"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession });
|
|
7786
|
+
} catch (error) {
|
|
7787
|
+
console.log(`room tasks failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7788
|
+
}
|
|
7789
|
+
continue;
|
|
7790
|
+
}
|
|
7791
|
+
|
|
7708
7792
|
if (line.startsWith("/room add ")) {
|
|
7709
7793
|
const raw = line.replace("/room add", "").trim();
|
|
7710
7794
|
if (!raw) {
|
|
@@ -7721,7 +7805,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7721
7805
|
try {
|
|
7722
7806
|
const positional = ["room", "add", memberId, role];
|
|
7723
7807
|
if (displayName) positional.push(...displayName.split(" "));
|
|
7724
|
-
await runRoomCommand(positional, { cwd: context.cwd, asJson: false });
|
|
7808
|
+
await runRoomCommand(positional, { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession });
|
|
7725
7809
|
} catch (error) {
|
|
7726
7810
|
console.log(`room add failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7727
7811
|
}
|
|
@@ -7735,13 +7819,42 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7735
7819
|
continue;
|
|
7736
7820
|
}
|
|
7737
7821
|
try {
|
|
7738
|
-
await runRoomCommand(["room", "remove", memberId], { cwd: context.cwd, asJson: false });
|
|
7822
|
+
await runRoomCommand(["room", "remove", memberId], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession });
|
|
7739
7823
|
} catch (error) {
|
|
7740
7824
|
console.log(`room remove failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7741
7825
|
}
|
|
7742
7826
|
continue;
|
|
7743
7827
|
}
|
|
7744
7828
|
|
|
7829
|
+
if (line.startsWith("/room task add ")) {
|
|
7830
|
+
const text = line.replace("/room task add", "").trim();
|
|
7831
|
+
if (!text) {
|
|
7832
|
+
console.log("Usage: /room task add <text>");
|
|
7833
|
+
continue;
|
|
7834
|
+
}
|
|
7835
|
+
try {
|
|
7836
|
+
await runRoomCommand(["room", "task", "add", ...text.split(" ")], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession });
|
|
7837
|
+
} catch (error) {
|
|
7838
|
+
console.log(`room task add failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7839
|
+
}
|
|
7840
|
+
continue;
|
|
7841
|
+
}
|
|
7842
|
+
|
|
7843
|
+
if (line.startsWith("/room task move ")) {
|
|
7844
|
+
const raw = line.replace("/room task move", "").trim();
|
|
7845
|
+
const [taskId, state] = raw.split(/\s+/, 2);
|
|
7846
|
+
if (!taskId || !state) {
|
|
7847
|
+
console.log("Usage: /room task move <id> <open|active|blocked|done>");
|
|
7848
|
+
continue;
|
|
7849
|
+
}
|
|
7850
|
+
try {
|
|
7851
|
+
await runRoomCommand(["room", "task", "move", taskId, state], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession });
|
|
7852
|
+
} catch (error) {
|
|
7853
|
+
console.log(`room task move failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7854
|
+
}
|
|
7855
|
+
continue;
|
|
7856
|
+
}
|
|
7857
|
+
|
|
7745
7858
|
if (line.startsWith("/room mode ")) {
|
|
7746
7859
|
const nextMode = line.replace("/room mode", "").trim().toLowerCase();
|
|
7747
7860
|
if (!nextMode) {
|
|
@@ -7749,7 +7862,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7749
7862
|
continue;
|
|
7750
7863
|
}
|
|
7751
7864
|
try {
|
|
7752
|
-
await runRoomCommand(["room", "mode", nextMode], { cwd: context.cwd, asJson: false });
|
|
7865
|
+
await runRoomCommand(["room", "mode", nextMode], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession });
|
|
7753
7866
|
} catch (error) {
|
|
7754
7867
|
console.log(`room mode failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7755
7868
|
}
|
|
@@ -7758,7 +7871,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7758
7871
|
|
|
7759
7872
|
if (line === "/room claim") {
|
|
7760
7873
|
try {
|
|
7761
|
-
await runRoomCommand(["room", "claim"], { cwd: context.cwd, asJson: false });
|
|
7874
|
+
await runRoomCommand(["room", "claim"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession });
|
|
7762
7875
|
} catch (error) {
|
|
7763
7876
|
console.log(`room claim failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7764
7877
|
}
|
|
@@ -7767,7 +7880,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7767
7880
|
|
|
7768
7881
|
if (line === "/room release") {
|
|
7769
7882
|
try {
|
|
7770
|
-
await runRoomCommand(["room", "release"], { cwd: context.cwd, asJson: false });
|
|
7883
|
+
await runRoomCommand(["room", "release"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession });
|
|
7771
7884
|
} catch (error) {
|
|
7772
7885
|
console.log(`room release failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7773
7886
|
}
|
|
@@ -9596,7 +9709,7 @@ export async function runCli(argv) {
|
|
|
9596
9709
|
}
|
|
9597
9710
|
|
|
9598
9711
|
if (command === "room") {
|
|
9599
|
-
await runRoomCommand(positional, { cwd: startupCwd, asJson });
|
|
9712
|
+
await runRoomCommand(positional, { cwd: startupCwd, asJson, runtime });
|
|
9600
9713
|
return;
|
|
9601
9714
|
}
|
|
9602
9715
|
|
package/src/gateway.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createSession, listSessions, loadSession, saveSession } from "./session
|
|
|
8
8
|
import { DEFAULT_PENDING_PAIRING_TTL_MINUTES, loadGatewayBridge, loadGatewayState, prunePendingPairings, saveGatewayBridge, saveGatewayState } from "./gateway-state.js";
|
|
9
9
|
import { getGatewayStatus, getChannelSpec } from "./channels.js";
|
|
10
10
|
import { canonicalizeLoosePath } from "./path-utils.js";
|
|
11
|
-
import { claimSharedOperator, getSharedMember, loadSharedProject, releaseSharedOperator, setSharedRoom, setSharedRoomMode, upsertSharedMember, removeSharedMember } from "./shared-project.js";
|
|
11
|
+
import { addSharedTask, claimSharedOperator, getSharedMember, listSharedTasks, loadSharedProject, moveSharedTask, releaseSharedOperator, setSharedRoom, setSharedRoomMode, upsertSharedMember, removeSharedMember } from "./shared-project.js";
|
|
12
12
|
|
|
13
13
|
const execFileAsync = promisify(execFile);
|
|
14
14
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -27,6 +27,7 @@ const TELEGRAM_COMMANDS = [
|
|
|
27
27
|
{ command: "runtime", description: "Show active runtime status" },
|
|
28
28
|
{ command: "room", description: "Show shared room status" },
|
|
29
29
|
{ command: "members", description: "List shared room members" },
|
|
30
|
+
{ command: "tasks", description: "List shared Roundtable tasks" },
|
|
30
31
|
{ command: "mode", description: "Show or set shared room mode" },
|
|
31
32
|
{ command: "claim", description: "Claim operator control for a shared project" },
|
|
32
33
|
{ command: "release", description: "Release operator control for a shared project" },
|
|
@@ -201,6 +202,9 @@ function buildRemoteHelp() {
|
|
|
201
202
|
"<code>/runtime</code> show active provider/model/runtime state",
|
|
202
203
|
"<code>/room</code> show shared project room status",
|
|
203
204
|
"<code>/members</code> list shared project members",
|
|
205
|
+
"<code>/tasks</code> list shared project tasks",
|
|
206
|
+
"<code>/task add <text></code> add a shared Roundtable task",
|
|
207
|
+
"<code>/task move <id> <open|active|blocked|done></code> move a shared Roundtable task",
|
|
204
208
|
"<code>/invite <user-id> [owner|editor|observer]</code> add or update a shared project member",
|
|
205
209
|
"<code>/remove-member <user-id></code> remove a shared project member",
|
|
206
210
|
"<code>/mode</code> or <code>/mode <chat|plan|execute></code> inspect or change shared room mode",
|
|
@@ -270,7 +274,7 @@ function formatSessionListMarkup(currentSessionId, sessions = []) {
|
|
|
270
274
|
return lines.join("\n");
|
|
271
275
|
}
|
|
272
276
|
|
|
273
|
-
function formatTelegramRoomMarkup(project) {
|
|
277
|
+
function formatTelegramRoomMarkup(project, options = {}) {
|
|
274
278
|
if (!project?.enabled) {
|
|
275
279
|
return "<b>Shared room</b>\nThis project is not shared.";
|
|
276
280
|
}
|
|
@@ -284,6 +288,18 @@ function formatTelegramRoomMarkup(project) {
|
|
|
284
288
|
const memberLines = members.length
|
|
285
289
|
? members.map((member) => `• ${escapeTelegramHtml(member.name || member.id)} <i>(${escapeTelegramHtml(member.role || "editor")})</i>`).join("\n")
|
|
286
290
|
: "• none";
|
|
291
|
+
const executor = options.executor || {};
|
|
292
|
+
const executorBits = [
|
|
293
|
+
`surface: <code>${escapeTelegramHtml(executor.surface || "telegram")}</code>`,
|
|
294
|
+
`provider: <code>${escapeTelegramHtml(executor.provider || "unknown")}</code>`,
|
|
295
|
+
`model: <code>${escapeTelegramHtml(executor.model || "unknown")}</code>`
|
|
296
|
+
];
|
|
297
|
+
if (executor.runtimeProfile) {
|
|
298
|
+
executorBits.push(`runtime profile: <code>${escapeTelegramHtml(executor.runtimeProfile)}</code>`);
|
|
299
|
+
}
|
|
300
|
+
if (executor.hostSessionId) {
|
|
301
|
+
executorBits.push(`host session: <code>${escapeTelegramHtml(executor.hostSessionId)}</code>`);
|
|
302
|
+
}
|
|
287
303
|
return [
|
|
288
304
|
"<b>Shared room</b>",
|
|
289
305
|
`project: <code>${escapeTelegramHtml(project.projectName || "")}</code>`,
|
|
@@ -291,6 +307,8 @@ function formatTelegramRoomMarkup(project) {
|
|
|
291
307
|
`room mode: <code>${escapeTelegramHtml(project.roomMode || "chat")}</code>`,
|
|
292
308
|
`room: <code>${escapeTelegramHtml(roomLabel)}</code>`,
|
|
293
309
|
`active operator: <code>${escapeTelegramHtml(active)}</code>`,
|
|
310
|
+
"<b>Executor</b>",
|
|
311
|
+
...executorBits,
|
|
294
312
|
"<b>Members</b>",
|
|
295
313
|
memberLines
|
|
296
314
|
].join("\n");
|
|
@@ -310,6 +328,45 @@ function formatTelegramMembersMarkup(project) {
|
|
|
310
328
|
].join("\n");
|
|
311
329
|
}
|
|
312
330
|
|
|
331
|
+
function formatTelegramTasksMarkup(tasks = []) {
|
|
332
|
+
if (!tasks.length) {
|
|
333
|
+
return "<b>Shared tasks</b>\n• none";
|
|
334
|
+
}
|
|
335
|
+
return [
|
|
336
|
+
"<b>Shared tasks</b>",
|
|
337
|
+
...tasks.map((task) => `• <code>${escapeTelegramHtml(task.id)}</code> <i>(${escapeTelegramHtml(task.state)})</i> ${escapeTelegramHtml(task.text)}`)
|
|
338
|
+
].join("\n");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function classifyTelegramGroupIntent(text = "") {
|
|
342
|
+
const normalized = String(text || "").trim();
|
|
343
|
+
const lower = normalized.toLowerCase();
|
|
344
|
+
if (!normalized) return { kind: "chat", reason: "empty" };
|
|
345
|
+
if (/^(hi|hello|hey|yo|thanks|thank you|cool|nice|ok|okay|k)\b/.test(lower)) {
|
|
346
|
+
return { kind: "chat", reason: "casual language" };
|
|
347
|
+
}
|
|
348
|
+
if (
|
|
349
|
+
lower.includes("brainstorm")
|
|
350
|
+
|| lower.includes("what do you think")
|
|
351
|
+
|| lower.includes("should we")
|
|
352
|
+
|| lower.includes("could we")
|
|
353
|
+
|| lower.includes("let's think")
|
|
354
|
+
|| lower.includes("plan this")
|
|
355
|
+
|| lower.includes("ideas for")
|
|
356
|
+
|| normalized.endsWith("?")
|
|
357
|
+
) {
|
|
358
|
+
return { kind: "plan", reason: "planning language" };
|
|
359
|
+
}
|
|
360
|
+
if (
|
|
361
|
+
/^(make|create|build|implement|fix|change|update|add|remove|delete|rename|refactor|write|edit|move|run|search|open|use|switch|generate|ship|deploy|debug|investigate|audit)\b/.test(lower)
|
|
362
|
+
|| /[`/].+/.test(normalized)
|
|
363
|
+
|| /\b(file|folder|directory|repo|project|code|command|function|component|page|route|api|schema|bug)\b/.test(lower)
|
|
364
|
+
) {
|
|
365
|
+
return { kind: "execution", reason: "explicit work request" };
|
|
366
|
+
}
|
|
367
|
+
return { kind: "chat", reason: "default non-execution" };
|
|
368
|
+
}
|
|
369
|
+
|
|
313
370
|
function parseInviteCommand(text) {
|
|
314
371
|
const parts = String(text || "").trim().split(/\s+/).filter(Boolean);
|
|
315
372
|
const userId = String(parts[1] || "").trim();
|
|
@@ -682,6 +739,19 @@ class TelegramGateway {
|
|
|
682
739
|
return { session, project: next };
|
|
683
740
|
}
|
|
684
741
|
|
|
742
|
+
async buildRoomMarkup(message, sessionId) {
|
|
743
|
+
const { project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
744
|
+
const host = await this.getLiveBridgeHost();
|
|
745
|
+
const executor = {
|
|
746
|
+
surface: host ? "live-tui" : "telegram-fallback",
|
|
747
|
+
provider: this.runtime.provider,
|
|
748
|
+
model: this.runtime.model,
|
|
749
|
+
runtimeProfile: this.channel.defaultRuntimeProfile || this.gateway.defaultRuntimeProfile || "",
|
|
750
|
+
hostSessionId: host?.sessionId || ""
|
|
751
|
+
};
|
|
752
|
+
return project?.enabled ? formatTelegramRoomMarkup(project, { executor }) : "<b>Shared room</b>\nThis project is not shared.";
|
|
753
|
+
}
|
|
754
|
+
|
|
685
755
|
async ensureSharedOperator(message, sessionId) {
|
|
686
756
|
const { session, project } = await this.loadSharedProjectForSession(sessionId);
|
|
687
757
|
if (!project?.enabled) return { ok: true, project: null, session };
|
|
@@ -939,8 +1009,7 @@ class TelegramGateway {
|
|
|
939
1009
|
}
|
|
940
1010
|
|
|
941
1011
|
if (text === "/room") {
|
|
942
|
-
|
|
943
|
-
await this.sendMessage(message.chat.id, project?.enabled ? formatTelegramRoomMarkup(project) : "<b>Shared room</b>\nThis project is not shared.", message.message_id);
|
|
1012
|
+
await this.sendMessage(message.chat.id, await this.buildRoomMarkup(message, sessionId), message.message_id);
|
|
944
1013
|
return;
|
|
945
1014
|
}
|
|
946
1015
|
|
|
@@ -950,6 +1019,17 @@ class TelegramGateway {
|
|
|
950
1019
|
return;
|
|
951
1020
|
}
|
|
952
1021
|
|
|
1022
|
+
if (text === "/tasks") {
|
|
1023
|
+
const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1024
|
+
if (!project?.enabled) {
|
|
1025
|
+
await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
const tasks = await listSharedTasks(session.cwd || this.cwd);
|
|
1029
|
+
await this.sendMessage(message.chat.id, formatTelegramTasksMarkup(tasks), message.message_id);
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
953
1033
|
if (text === "/mode") {
|
|
954
1034
|
const { project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
955
1035
|
await this.sendMessage(
|
|
@@ -1004,7 +1084,7 @@ class TelegramGateway {
|
|
|
1004
1084
|
}
|
|
1005
1085
|
try {
|
|
1006
1086
|
const released = await releaseSharedOperator(session.cwd || this.cwd, userId, { actorId: userId });
|
|
1007
|
-
await this.sendMessage(message.chat.id, released.activeOperator?.id ?
|
|
1087
|
+
await this.sendMessage(message.chat.id, released.activeOperator?.id ? await this.buildRoomMarkup(message, sessionId) : "Shared room released.", message.message_id);
|
|
1008
1088
|
} catch (error) {
|
|
1009
1089
|
await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
|
|
1010
1090
|
}
|
|
@@ -1064,6 +1144,46 @@ class TelegramGateway {
|
|
|
1064
1144
|
return;
|
|
1065
1145
|
}
|
|
1066
1146
|
|
|
1147
|
+
if (text.startsWith("/task add ")) {
|
|
1148
|
+
const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1149
|
+
if (!project?.enabled) {
|
|
1150
|
+
await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
const taskText = text.replace("/task add", "").trim();
|
|
1154
|
+
if (!taskText) {
|
|
1155
|
+
await this.sendMessage(message.chat.id, "Usage: /task add <text>", message.message_id);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
try {
|
|
1159
|
+
const result = await addSharedTask(session.cwd || this.cwd, taskText, { actorId: userId });
|
|
1160
|
+
await this.sendMessage(message.chat.id, `Added shared task <code>${escapeTelegramHtml(result.task.id)}</code>`, message.message_id);
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
|
|
1163
|
+
}
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (text.startsWith("/task move ")) {
|
|
1168
|
+
const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1169
|
+
if (!project?.enabled) {
|
|
1170
|
+
await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const [taskId, state] = text.replace("/task move", "").trim().split(/\s+/, 2);
|
|
1174
|
+
if (!taskId || !state) {
|
|
1175
|
+
await this.sendMessage(message.chat.id, "Usage: /task move <id> <open|active|blocked|done>", message.message_id);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
try {
|
|
1179
|
+
const result = await moveSharedTask(session.cwd || this.cwd, taskId, state, { actorId: userId });
|
|
1180
|
+
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);
|
|
1181
|
+
} catch (error) {
|
|
1182
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
|
|
1183
|
+
}
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1067
1187
|
if (text === "/runtime") {
|
|
1068
1188
|
const status = await this.runRuntimeStatus();
|
|
1069
1189
|
await this.sendMessage(message.chat.id, formatRuntimeStatus(status), message.message_id);
|
|
@@ -1171,16 +1291,29 @@ class TelegramGateway {
|
|
|
1171
1291
|
return;
|
|
1172
1292
|
}
|
|
1173
1293
|
|
|
1294
|
+
const promptText = this.stripBotMention(text);
|
|
1295
|
+
const groupIntent = this.isGroupChat(message) ? classifyTelegramGroupIntent(promptText) : { kind: "execution", reason: "private chat" };
|
|
1296
|
+
if (this.isGroupChat(message) && groupIntent.kind !== "execution") {
|
|
1297
|
+
const planHint = groupIntent.kind === "plan"
|
|
1298
|
+
? "Planning/brainstorming message detected. No execution will run in the group. Use /task add to capture work, or switch to /mode execute and send an explicit execution request."
|
|
1299
|
+
: "Chat message detected. No execution will run in the group. Use /task add to capture work or send an explicit execution request when you want the bot to act.";
|
|
1300
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(planHint), message.message_id, { parseMode: "HTML" });
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1174
1304
|
const stopTyping = await this.startTypingLoop(message.chat.id);
|
|
1175
1305
|
let previewMessage = null;
|
|
1176
1306
|
try {
|
|
1177
1307
|
const operatorGate = await this.ensureSharedOperator(message, sessionId);
|
|
1178
1308
|
if (!operatorGate.ok) {
|
|
1179
|
-
|
|
1309
|
+
const gateReason =
|
|
1310
|
+
this.isGroupChat(message) && groupIntent.kind === "execution" && operatorGate.project?.enabled && operatorGate.project?.roomMode !== "execute"
|
|
1311
|
+
? `Execution request detected, but the shared room is in ${operatorGate.project?.roomMode || "chat"} mode. Switch to /mode execute before asking Waterbrother to change code.`
|
|
1312
|
+
: operatorGate.reason || "Shared room is not available.";
|
|
1313
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(gateReason), message.message_id, { parseMode: "HTML" });
|
|
1180
1314
|
return;
|
|
1181
1315
|
}
|
|
1182
1316
|
previewMessage = await this.sendProgressMessage(message.chat.id, message.message_id);
|
|
1183
|
-
const promptText = this.stripBotMention(text);
|
|
1184
1317
|
const content = (await this.runPromptViaBridge(message, sessionId, promptText))
|
|
1185
1318
|
?? (await this.runPromptFallback(sessionId, promptText));
|
|
1186
1319
|
await this.deliverPromptResult(message.chat.id, message.message_id, previewMessage, content);
|
package/src/shared-project.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
|
|
6
6
|
const SHARED_FILE = path.join(".waterbrother", "shared.json");
|
|
7
7
|
const ROUNDTABLE_FILE = "ROUNDTABLE.md";
|
|
8
|
+
const TASK_STATES = ["open", "active", "blocked", "done"];
|
|
8
9
|
|
|
9
10
|
function normalizeMember(member = {}) {
|
|
10
11
|
return {
|
|
@@ -25,6 +26,11 @@ function memberRoleWeight(role = "") {
|
|
|
25
26
|
|
|
26
27
|
function normalizeSharedProject(project = {}, cwd = process.cwd()) {
|
|
27
28
|
const members = Array.isArray(project.members) ? project.members.map(normalizeMember).filter((item) => item.id) : [];
|
|
29
|
+
const tasks = Array.isArray(project.tasks)
|
|
30
|
+
? project.tasks
|
|
31
|
+
.map((task) => normalizeSharedTask(task))
|
|
32
|
+
.filter((task) => task.id && task.text)
|
|
33
|
+
: [];
|
|
28
34
|
const activeOperator = project.activeOperator && typeof project.activeOperator === "object"
|
|
29
35
|
? {
|
|
30
36
|
id: String(project.activeOperator.id || "").trim(),
|
|
@@ -49,6 +55,7 @@ function normalizeSharedProject(project = {}, cwd = process.cwd()) {
|
|
|
49
55
|
? String(project.roomMode).trim()
|
|
50
56
|
: "chat",
|
|
51
57
|
members,
|
|
58
|
+
tasks,
|
|
52
59
|
activeOperator: activeOperator?.id ? activeOperator : null,
|
|
53
60
|
approvalPolicy: String(project.approvalPolicy || "owner").trim() || "owner",
|
|
54
61
|
createdAt: String(project.createdAt || new Date().toISOString()).trim(),
|
|
@@ -56,6 +63,18 @@ function normalizeSharedProject(project = {}, cwd = process.cwd()) {
|
|
|
56
63
|
};
|
|
57
64
|
}
|
|
58
65
|
|
|
66
|
+
function normalizeSharedTask(task = {}) {
|
|
67
|
+
return {
|
|
68
|
+
id: String(task.id || `rt_${crypto.randomBytes(3).toString("hex")}`).trim(),
|
|
69
|
+
text: String(task.text || "").trim(),
|
|
70
|
+
state: TASK_STATES.includes(String(task.state || "").trim()) ? String(task.state).trim() : "open",
|
|
71
|
+
createdAt: String(task.createdAt || new Date().toISOString()).trim(),
|
|
72
|
+
updatedAt: String(task.updatedAt || new Date().toISOString()).trim(),
|
|
73
|
+
createdBy: String(task.createdBy || "").trim(),
|
|
74
|
+
assignedTo: String(task.assignedTo || "").trim()
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
function sharedFilePath(cwd) {
|
|
60
79
|
return path.join(cwd, SHARED_FILE);
|
|
61
80
|
}
|
|
@@ -90,6 +109,11 @@ function defaultRoundtableContent(project) {
|
|
|
90
109
|
const activeOperator = project.activeOperator?.id
|
|
91
110
|
? `${project.activeOperator.name || project.activeOperator.id}`
|
|
92
111
|
: "none";
|
|
112
|
+
const taskLines = TASK_STATES.flatMap((state) => {
|
|
113
|
+
const tasks = (project.tasks || []).filter((task) => task.state === state);
|
|
114
|
+
if (!tasks.length) return [`- ${state}: -`];
|
|
115
|
+
return [`- ${state}:`, ...tasks.map((task) => ` - [${task.id}] ${task.text}`)];
|
|
116
|
+
});
|
|
93
117
|
return [
|
|
94
118
|
"# Roundtable",
|
|
95
119
|
"",
|
|
@@ -116,14 +140,35 @@ function defaultRoundtableContent(project) {
|
|
|
116
140
|
"-",
|
|
117
141
|
"",
|
|
118
142
|
"## Task Queue",
|
|
119
|
-
|
|
120
|
-
"- active: -",
|
|
121
|
-
"- blocked: -",
|
|
122
|
-
"- done: -",
|
|
143
|
+
...taskLines,
|
|
123
144
|
""
|
|
124
145
|
].join("\n");
|
|
125
146
|
}
|
|
126
147
|
|
|
148
|
+
function buildRoundtableTaskSection(project) {
|
|
149
|
+
const taskLines = TASK_STATES.flatMap((state) => {
|
|
150
|
+
const tasks = (project.tasks || []).filter((task) => task.state === state);
|
|
151
|
+
if (!tasks.length) return [`- ${state}: -`];
|
|
152
|
+
return [`- ${state}:`, ...tasks.map((task) => ` - [${task.id}] ${task.text}`)];
|
|
153
|
+
});
|
|
154
|
+
return ["## Task Queue", ...taskLines, ""].join("\n");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function syncRoundtableTaskSection(cwd, project) {
|
|
158
|
+
const target = roundtablePath(cwd);
|
|
159
|
+
if (!(await pathExists(target))) return null;
|
|
160
|
+
const current = await fs.readFile(target, "utf8");
|
|
161
|
+
const nextSection = buildRoundtableTaskSection(project);
|
|
162
|
+
let nextContent;
|
|
163
|
+
if (/^## Task Queue\s*$/m.test(current)) {
|
|
164
|
+
nextContent = current.replace(/## Task Queue[\s\S]*$/, nextSection);
|
|
165
|
+
} else {
|
|
166
|
+
nextContent = `${current.trimEnd()}\n\n${nextSection}`;
|
|
167
|
+
}
|
|
168
|
+
await fs.writeFile(target, nextContent.endsWith("\n") ? nextContent : `${nextContent}\n`, "utf8");
|
|
169
|
+
return target;
|
|
170
|
+
}
|
|
171
|
+
|
|
127
172
|
export async function ensureRoundtable(cwd, project) {
|
|
128
173
|
const target = roundtablePath(cwd);
|
|
129
174
|
if (await pathExists(target)) return target;
|
|
@@ -156,6 +201,7 @@ export async function saveSharedProject(cwd, project) {
|
|
|
156
201
|
}, cwd);
|
|
157
202
|
await writeJsonAtomically(sharedFilePath(cwd), next);
|
|
158
203
|
await ensureRoundtable(cwd, next);
|
|
204
|
+
await syncRoundtableTaskSection(cwd, next);
|
|
159
205
|
return next;
|
|
160
206
|
}
|
|
161
207
|
|
|
@@ -247,6 +293,20 @@ function requireOwner(project, actorId = "") {
|
|
|
247
293
|
}
|
|
248
294
|
}
|
|
249
295
|
|
|
296
|
+
function requireEditor(project, actorId = "") {
|
|
297
|
+
requireSharedProject(project);
|
|
298
|
+
if (!memberHasAtLeastRole(project, actorId, "editor")) {
|
|
299
|
+
throw new Error("Only a shared-project owner or editor can do that.");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function requireMember(project, actorId = "") {
|
|
304
|
+
requireSharedProject(project);
|
|
305
|
+
if (!getSharedMember(project, actorId)) {
|
|
306
|
+
throw new Error("Only a shared-project member can do that.");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
250
310
|
export async function listSharedMembers(cwd) {
|
|
251
311
|
const project = await loadSharedProject(cwd);
|
|
252
312
|
requireSharedProject(project);
|
|
@@ -299,6 +359,56 @@ export async function removeSharedMember(cwd, memberId = "", options = {}) {
|
|
|
299
359
|
return next;
|
|
300
360
|
}
|
|
301
361
|
|
|
362
|
+
export async function listSharedTasks(cwd) {
|
|
363
|
+
const project = await loadSharedProject(cwd);
|
|
364
|
+
requireSharedProject(project);
|
|
365
|
+
return project.tasks || [];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export async function addSharedTask(cwd, text = "", options = {}) {
|
|
369
|
+
const existing = await loadSharedProject(cwd);
|
|
370
|
+
requireMember(existing, options.actorId);
|
|
371
|
+
const normalizedText = String(text || "").trim();
|
|
372
|
+
if (!normalizedText) throw new Error("task text is required");
|
|
373
|
+
const task = normalizeSharedTask({
|
|
374
|
+
text: normalizedText,
|
|
375
|
+
state: "open",
|
|
376
|
+
createdBy: String(options.actorId || "").trim()
|
|
377
|
+
});
|
|
378
|
+
const next = await saveSharedProject(cwd, {
|
|
379
|
+
...existing,
|
|
380
|
+
tasks: [...(existing.tasks || []), task]
|
|
381
|
+
});
|
|
382
|
+
await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: task added [${task.id}] ${task.text}`);
|
|
383
|
+
return { project: next, task };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export async function moveSharedTask(cwd, taskId = "", state = "open", options = {}) {
|
|
387
|
+
const existing = await loadSharedProject(cwd);
|
|
388
|
+
requireEditor(existing, options.actorId);
|
|
389
|
+
const normalizedId = String(taskId || "").trim();
|
|
390
|
+
const normalizedState = String(state || "").trim().toLowerCase();
|
|
391
|
+
if (!normalizedId) throw new Error("task id is required");
|
|
392
|
+
if (!TASK_STATES.includes(normalizedState)) {
|
|
393
|
+
throw new Error(`Invalid task state. Expected one of ${TASK_STATES.join(", ")}.`);
|
|
394
|
+
}
|
|
395
|
+
const tasks = [...(existing.tasks || [])];
|
|
396
|
+
const index = tasks.findIndex((task) => task.id === normalizedId);
|
|
397
|
+
if (index < 0) throw new Error(`No shared task found for ${normalizedId}`);
|
|
398
|
+
tasks[index] = {
|
|
399
|
+
...tasks[index],
|
|
400
|
+
state: normalizedState,
|
|
401
|
+
updatedAt: new Date().toISOString(),
|
|
402
|
+
assignedTo: normalizedState === "active" ? String(options.actorId || "").trim() : tasks[index].assignedTo
|
|
403
|
+
};
|
|
404
|
+
const next = await saveSharedProject(cwd, {
|
|
405
|
+
...existing,
|
|
406
|
+
tasks
|
|
407
|
+
});
|
|
408
|
+
await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: task moved [${tasks[index].id}] -> ${normalizedState}`);
|
|
409
|
+
return { project: next, task: tasks[index] };
|
|
410
|
+
}
|
|
411
|
+
|
|
302
412
|
export async function claimSharedOperator(cwd, operator = {}) {
|
|
303
413
|
const existing = await loadSharedProject(cwd);
|
|
304
414
|
requireSharedProject(existing);
|
|
@@ -361,7 +471,8 @@ export function formatSharedProjectStatus(project) {
|
|
|
361
471
|
roomMode: project.roomMode,
|
|
362
472
|
approvalPolicy: project.approvalPolicy,
|
|
363
473
|
activeOperator: project.activeOperator,
|
|
364
|
-
members: project.members
|
|
474
|
+
members: project.members,
|
|
475
|
+
tasks: project.tasks
|
|
365
476
|
}, null, 2);
|
|
366
477
|
}
|
|
367
478
|
|