@tritard/waterbrother 0.16.28 → 0.16.30
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 +6 -1
- package/package.json +1 -1
- package/src/cli.js +154 -17
- package/src/gateway-state.js +1 -0
- package/src/gateway.js +211 -8
- package/src/shared-project.js +68 -2
package/README.md
CHANGED
|
@@ -267,6 +267,8 @@ Shared project foundation is now live:
|
|
|
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
269
|
- manage the shared backlog with `waterbrother room tasks`, `waterbrother room task add`, and `waterbrother room task move`
|
|
270
|
+
- assign and claim shared work with `waterbrother room task assign` and `waterbrother room task claim`
|
|
271
|
+
- choose a shared execution preset with `waterbrother room runtime <profile>`
|
|
270
272
|
- claim or release the shared operator lock with `waterbrother room claim` and `waterbrother room release`
|
|
271
273
|
- shared project metadata lives in `.waterbrother/shared.json`
|
|
272
274
|
- human collaboration notes live in `ROUNDTABLE.md`
|
|
@@ -279,10 +281,13 @@ Current Telegram behavior:
|
|
|
279
281
|
- pending pairings are explicit and expire automatically after 12 hours unless approved
|
|
280
282
|
- paired Telegram users drive the same live session and permissions as the terminal operator when the TUI bridge is attached
|
|
281
283
|
- Telegram now supports remote workspace control with `/cwd`, `/use <path>`, `/desktop`, and `/new-project <name>`
|
|
282
|
-
- shared projects now support `/room`, `/members`, `/tasks`, `/mode`, `/claim`, `/release`, `/invite`, `/remove-member`, and `/task ...` from Telegram
|
|
284
|
+
- shared projects now support `/room`, `/members`, `/tasks`, `/mode`, `/claim`, `/release`, `/invite`, `/remove-member`, `/room-runtime`, and `/task ...` from Telegram
|
|
283
285
|
- shared Telegram execution only runs when the shared room is in `execute` mode
|
|
284
286
|
- room administration is owner-only, and only owners/editors can hold the operator lock
|
|
287
|
+
- `/room` status now shows the active executor surface plus provider/model/runtime identity
|
|
285
288
|
- in Telegram groups, Waterbrother only responds when directly targeted: slash commands, `@botname` mentions, or replies to a bot message
|
|
289
|
+
- in Telegram groups, directly targeted messages are now classified as chat, planning, or execution; only explicit execution requests run the live session
|
|
290
|
+
- in shared plan-mode groups, targeted `task:` and `todo:` messages are captured directly into the Roundtable task queue
|
|
286
291
|
- pairing is now explicit: first DM creates a pending request, then approve locally with `waterbrother gateway pair telegram <user-id>`
|
|
287
292
|
|
|
288
293
|
## Release flow
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -59,7 +59,9 @@ import { formatPlanForDisplay } from "./planner.js";
|
|
|
59
59
|
import { parseCharterFromGoal, runExperimentLoop, formatExperimentSummary, gitReturnToBranch } from "./experiment.js";
|
|
60
60
|
import {
|
|
61
61
|
addSharedTask,
|
|
62
|
+
assignSharedTask,
|
|
62
63
|
claimSharedOperator,
|
|
64
|
+
claimSharedTask,
|
|
63
65
|
disableSharedProject,
|
|
64
66
|
enableSharedProject,
|
|
65
67
|
formatSharedProjectStatus,
|
|
@@ -70,6 +72,7 @@ import {
|
|
|
70
72
|
moveSharedTask,
|
|
71
73
|
releaseSharedOperator,
|
|
72
74
|
removeSharedMember,
|
|
75
|
+
setSharedRuntimeProfile,
|
|
73
76
|
setSharedRoomMode,
|
|
74
77
|
upsertSharedMember
|
|
75
78
|
} from "./shared-project.js";
|
|
@@ -159,7 +162,11 @@ const INTERACTIVE_COMMANDS = [
|
|
|
159
162
|
{ name: "/room remove <id>", description: "Remove a shared-project member" },
|
|
160
163
|
{ name: "/room tasks", description: "List Roundtable tasks for the shared project" },
|
|
161
164
|
{ name: "/room task add <text>", description: "Add a Roundtable task" },
|
|
165
|
+
{ name: "/room task assign <id> <member-id>", description: "Assign a Roundtable task to a shared member" },
|
|
166
|
+
{ name: "/room task claim <id>", description: "Claim a Roundtable task for yourself" },
|
|
162
167
|
{ name: "/room task move <id> <open|active|blocked|done>", description: "Move a Roundtable task between states" },
|
|
168
|
+
{ name: "/room runtime", description: "Show the shared room runtime profile" },
|
|
169
|
+
{ name: "/room runtime <name|clear>", description: "Set or clear the shared room runtime profile" },
|
|
163
170
|
{ name: "/room mode <chat|plan|execute>", description: "Set collaboration mode for the shared room" },
|
|
164
171
|
{ name: "/room claim", description: "Claim operator control for the shared room" },
|
|
165
172
|
{ name: "/room release", description: "Release operator control for the shared room" },
|
|
@@ -289,7 +296,10 @@ Usage:
|
|
|
289
296
|
waterbrother room remove <member-id>
|
|
290
297
|
waterbrother room tasks
|
|
291
298
|
waterbrother room task add <text>
|
|
299
|
+
waterbrother room task assign <id> <member-id>
|
|
300
|
+
waterbrother room task claim <id>
|
|
292
301
|
waterbrother room task move <id> <open|active|blocked|done>
|
|
302
|
+
waterbrother room runtime [<name>|clear]
|
|
293
303
|
waterbrother room mode <chat|plan|execute>
|
|
294
304
|
waterbrother room claim
|
|
295
305
|
waterbrother room release
|
|
@@ -2612,6 +2622,24 @@ function buildRuntimeStatusPayload(runtime, agent, currentSession = null) {
|
|
|
2612
2622
|
};
|
|
2613
2623
|
}
|
|
2614
2624
|
|
|
2625
|
+
function buildRoomStatusPayload(project, runtime = null, currentSession = null) {
|
|
2626
|
+
if (!project) {
|
|
2627
|
+
return { enabled: false };
|
|
2628
|
+
}
|
|
2629
|
+
return {
|
|
2630
|
+
...project,
|
|
2631
|
+
executor: {
|
|
2632
|
+
surface: "local-tui",
|
|
2633
|
+
roomRuntimeProfile: project.runtimeProfile || "",
|
|
2634
|
+
runtimeProfile: currentSession?.runtimeProfile || runtime?.gateway?.defaultRuntimeProfile || "",
|
|
2635
|
+
provider: runtime?.provider || "unknown",
|
|
2636
|
+
model: runtime?.model || "unknown",
|
|
2637
|
+
designModel: runtime?.designModel || "unknown",
|
|
2638
|
+
agentProfile: runtime?.agentProfile || "coder"
|
|
2639
|
+
}
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2615
2643
|
async function applyRuntimeSelection({
|
|
2616
2644
|
config,
|
|
2617
2645
|
context,
|
|
@@ -3774,16 +3802,17 @@ async function runProjectCommand(positional, { cwd = process.cwd(), asJson = fal
|
|
|
3774
3802
|
throw new Error("Usage: waterbrother project share|unshare");
|
|
3775
3803
|
}
|
|
3776
3804
|
|
|
3777
|
-
async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false } = {}) {
|
|
3805
|
+
async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false, runtime = null, currentSession = null, agent = null, context = null } = {}) {
|
|
3778
3806
|
const sub = String(positional[1] || "status").trim().toLowerCase();
|
|
3779
3807
|
const operator = getLocalOperatorIdentity();
|
|
3780
3808
|
if (sub === "status") {
|
|
3781
3809
|
const project = await loadSharedProject(cwd);
|
|
3810
|
+
const payload = buildRoomStatusPayload(project, runtime, currentSession);
|
|
3782
3811
|
if (asJson) {
|
|
3783
|
-
printData(
|
|
3812
|
+
printData(payload, true);
|
|
3784
3813
|
return;
|
|
3785
3814
|
}
|
|
3786
|
-
console.log(
|
|
3815
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3787
3816
|
return;
|
|
3788
3817
|
}
|
|
3789
3818
|
|
|
@@ -3814,7 +3843,7 @@ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false
|
|
|
3814
3843
|
return;
|
|
3815
3844
|
}
|
|
3816
3845
|
for (const task of tasks) {
|
|
3817
|
-
console.log(`${task.id}\t${task.state}\t${task.text}`);
|
|
3846
|
+
console.log(`${task.id}\t${task.state}\t${task.assignedTo || "-"}\t${task.text}`);
|
|
3818
3847
|
}
|
|
3819
3848
|
return;
|
|
3820
3849
|
}
|
|
@@ -3848,7 +3877,34 @@ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false
|
|
|
3848
3877
|
console.log(`Moved shared task [${result.task.id}] to ${result.task.state}`);
|
|
3849
3878
|
return;
|
|
3850
3879
|
}
|
|
3851
|
-
|
|
3880
|
+
if (action === "assign") {
|
|
3881
|
+
const taskId = String(positional[3] || "").trim();
|
|
3882
|
+
const memberId = String(positional[4] || "").trim();
|
|
3883
|
+
if (!taskId || !memberId) {
|
|
3884
|
+
throw new Error("Usage: waterbrother room task assign <id> <member-id>");
|
|
3885
|
+
}
|
|
3886
|
+
const result = await assignSharedTask(cwd, taskId, memberId, { actorId: operator.id });
|
|
3887
|
+
if (asJson) {
|
|
3888
|
+
printData({ ok: true, action: "task-assign", task: result.task, project: result.project }, true);
|
|
3889
|
+
return;
|
|
3890
|
+
}
|
|
3891
|
+
console.log(`Assigned shared task [${result.task.id}] to ${memberId}`);
|
|
3892
|
+
return;
|
|
3893
|
+
}
|
|
3894
|
+
if (action === "claim") {
|
|
3895
|
+
const taskId = String(positional[3] || "").trim();
|
|
3896
|
+
if (!taskId) {
|
|
3897
|
+
throw new Error("Usage: waterbrother room task claim <id>");
|
|
3898
|
+
}
|
|
3899
|
+
const result = await claimSharedTask(cwd, taskId, { actorId: operator.id });
|
|
3900
|
+
if (asJson) {
|
|
3901
|
+
printData({ ok: true, action: "task-claim", task: result.task, project: result.project }, true);
|
|
3902
|
+
return;
|
|
3903
|
+
}
|
|
3904
|
+
console.log(`Claimed shared task [${result.task.id}]`);
|
|
3905
|
+
return;
|
|
3906
|
+
}
|
|
3907
|
+
throw new Error("Usage: waterbrother room task add <text>|assign <id> <member-id>|claim <id>|move <id> <open|active|blocked|done>");
|
|
3852
3908
|
}
|
|
3853
3909
|
|
|
3854
3910
|
if (sub === "add") {
|
|
@@ -3925,7 +3981,35 @@ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false
|
|
|
3925
3981
|
return;
|
|
3926
3982
|
}
|
|
3927
3983
|
|
|
3928
|
-
|
|
3984
|
+
if (sub === "runtime") {
|
|
3985
|
+
const nextProfile = String(positional[2] || "").trim();
|
|
3986
|
+
if (!nextProfile) {
|
|
3987
|
+
const project = await loadSharedProject(cwd);
|
|
3988
|
+
if (asJson) {
|
|
3989
|
+
printData({ ok: true, runtimeProfile: project?.runtimeProfile || "", project }, true);
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
console.log(project?.runtimeProfile || "none");
|
|
3993
|
+
return;
|
|
3994
|
+
}
|
|
3995
|
+
const normalized = nextProfile.toLowerCase() === "clear" ? "" : nextProfile;
|
|
3996
|
+
const project = await setSharedRuntimeProfile(cwd, normalized, { actorId: operator.id });
|
|
3997
|
+
if (normalized && agent && context && currentSession) {
|
|
3998
|
+
const { config } = await loadConfigLayers(cwd);
|
|
3999
|
+
const loaded = await loadNamedRuntimeProfile(normalized, { cwd, config, runtime: context.runtime, context, agent, session: currentSession });
|
|
4000
|
+
if (!loaded) {
|
|
4001
|
+
throw new Error(`runtime profile "${normalized}" not found`);
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
if (asJson) {
|
|
4005
|
+
printData({ ok: true, action: "runtime", runtimeProfile: project.runtimeProfile, project }, true);
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
4008
|
+
console.log(project.runtimeProfile ? `Room runtime profile set to ${project.runtimeProfile}` : "Room runtime profile cleared");
|
|
4009
|
+
return;
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
throw new Error("Usage: waterbrother room status|members|add <member-id> [owner|editor|observer] [display name]|remove <member-id>|tasks|task add <text>|task assign <id> <member-id>|task claim <id>|task move <id> <open|active|blocked|done>|runtime [<name>|clear]|mode <chat|plan|execute>|claim|release");
|
|
3929
4013
|
}
|
|
3930
4014
|
|
|
3931
4015
|
async function runMcpCommand(positional, runtime, { cwd, asJson = false } = {}) {
|
|
@@ -7114,6 +7198,20 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7114
7198
|
|
|
7115
7199
|
remoteSessionId = currentSession.id;
|
|
7116
7200
|
await touchTelegramBridgeHost({ sessionId: currentSession.id, cwd: context.cwd });
|
|
7201
|
+
if (remoteRequest.runtimeProfile && remoteRequest.runtimeProfile !== currentSession.runtimeProfile) {
|
|
7202
|
+
const { config } = await loadConfigLayers(context.cwd);
|
|
7203
|
+
const loaded = await loadNamedRuntimeProfile(remoteRequest.runtimeProfile, {
|
|
7204
|
+
cwd: context.cwd,
|
|
7205
|
+
config,
|
|
7206
|
+
runtime: context.runtime,
|
|
7207
|
+
context,
|
|
7208
|
+
agent,
|
|
7209
|
+
session: currentSession
|
|
7210
|
+
});
|
|
7211
|
+
if (loaded) {
|
|
7212
|
+
console.log(cyan(`telegram runtime -> ${remoteRequest.runtimeProfile}`));
|
|
7213
|
+
}
|
|
7214
|
+
}
|
|
7117
7215
|
console.log(cyan(`telegram request from ${remoteActor}`));
|
|
7118
7216
|
console.log(`${cyan("telegram>")} ${sanitizeTerminalText(remoteRequest.text || "")}`);
|
|
7119
7217
|
await runTextTurnInteractive({
|
|
@@ -7746,7 +7844,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7746
7844
|
|
|
7747
7845
|
if (line === "/room") {
|
|
7748
7846
|
try {
|
|
7749
|
-
await runRoomCommand(["room", "status"], { cwd: context.cwd, asJson: false });
|
|
7847
|
+
await runRoomCommand(["room", "status"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
|
|
7750
7848
|
} catch (error) {
|
|
7751
7849
|
console.log(`room status failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7752
7850
|
}
|
|
@@ -7755,7 +7853,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7755
7853
|
|
|
7756
7854
|
if (line === "/room members") {
|
|
7757
7855
|
try {
|
|
7758
|
-
await runRoomCommand(["room", "members"], { cwd: context.cwd, asJson: false });
|
|
7856
|
+
await runRoomCommand(["room", "members"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
|
|
7759
7857
|
} catch (error) {
|
|
7760
7858
|
console.log(`room members failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7761
7859
|
}
|
|
@@ -7764,7 +7862,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7764
7862
|
|
|
7765
7863
|
if (line === "/room tasks") {
|
|
7766
7864
|
try {
|
|
7767
|
-
await runRoomCommand(["room", "tasks"], { cwd: context.cwd, asJson: false });
|
|
7865
|
+
await runRoomCommand(["room", "tasks"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
|
|
7768
7866
|
} catch (error) {
|
|
7769
7867
|
console.log(`room tasks failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7770
7868
|
}
|
|
@@ -7787,7 +7885,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7787
7885
|
try {
|
|
7788
7886
|
const positional = ["room", "add", memberId, role];
|
|
7789
7887
|
if (displayName) positional.push(...displayName.split(" "));
|
|
7790
|
-
await runRoomCommand(positional, { cwd: context.cwd, asJson: false });
|
|
7888
|
+
await runRoomCommand(positional, { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
|
|
7791
7889
|
} catch (error) {
|
|
7792
7890
|
console.log(`room add failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7793
7891
|
}
|
|
@@ -7801,7 +7899,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7801
7899
|
continue;
|
|
7802
7900
|
}
|
|
7803
7901
|
try {
|
|
7804
|
-
await runRoomCommand(["room", "remove", memberId], { cwd: context.cwd, asJson: false });
|
|
7902
|
+
await runRoomCommand(["room", "remove", memberId], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
|
|
7805
7903
|
} catch (error) {
|
|
7806
7904
|
console.log(`room remove failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7807
7905
|
}
|
|
@@ -7815,7 +7913,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7815
7913
|
continue;
|
|
7816
7914
|
}
|
|
7817
7915
|
try {
|
|
7818
|
-
await runRoomCommand(["room", "task", "add", ...text.split(" ")], { cwd: context.cwd, asJson: false });
|
|
7916
|
+
await runRoomCommand(["room", "task", "add", ...text.split(" ")], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
|
|
7819
7917
|
} catch (error) {
|
|
7820
7918
|
console.log(`room task add failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7821
7919
|
}
|
|
@@ -7830,13 +7928,42 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7830
7928
|
continue;
|
|
7831
7929
|
}
|
|
7832
7930
|
try {
|
|
7833
|
-
await runRoomCommand(["room", "task", "move", taskId, state], { cwd: context.cwd, asJson: false });
|
|
7931
|
+
await runRoomCommand(["room", "task", "move", taskId, state], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
|
|
7834
7932
|
} catch (error) {
|
|
7835
7933
|
console.log(`room task move failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7836
7934
|
}
|
|
7837
7935
|
continue;
|
|
7838
7936
|
}
|
|
7839
7937
|
|
|
7938
|
+
if (line.startsWith("/room task assign ")) {
|
|
7939
|
+
const raw = line.replace("/room task assign", "").trim();
|
|
7940
|
+
const [taskId, memberId] = raw.split(/\s+/, 2);
|
|
7941
|
+
if (!taskId || !memberId) {
|
|
7942
|
+
console.log("Usage: /room task assign <id> <member-id>");
|
|
7943
|
+
continue;
|
|
7944
|
+
}
|
|
7945
|
+
try {
|
|
7946
|
+
await runRoomCommand(["room", "task", "assign", taskId, memberId], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
|
|
7947
|
+
} catch (error) {
|
|
7948
|
+
console.log(`room task assign failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7949
|
+
}
|
|
7950
|
+
continue;
|
|
7951
|
+
}
|
|
7952
|
+
|
|
7953
|
+
if (line.startsWith("/room task claim ")) {
|
|
7954
|
+
const taskId = line.replace("/room task claim", "").trim();
|
|
7955
|
+
if (!taskId) {
|
|
7956
|
+
console.log("Usage: /room task claim <id>");
|
|
7957
|
+
continue;
|
|
7958
|
+
}
|
|
7959
|
+
try {
|
|
7960
|
+
await runRoomCommand(["room", "task", "claim", taskId], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
|
|
7961
|
+
} catch (error) {
|
|
7962
|
+
console.log(`room task claim failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7963
|
+
}
|
|
7964
|
+
continue;
|
|
7965
|
+
}
|
|
7966
|
+
|
|
7840
7967
|
if (line.startsWith("/room mode ")) {
|
|
7841
7968
|
const nextMode = line.replace("/room mode", "").trim().toLowerCase();
|
|
7842
7969
|
if (!nextMode) {
|
|
@@ -7844,16 +7971,26 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7844
7971
|
continue;
|
|
7845
7972
|
}
|
|
7846
7973
|
try {
|
|
7847
|
-
await runRoomCommand(["room", "mode", nextMode], { cwd: context.cwd, asJson: false });
|
|
7974
|
+
await runRoomCommand(["room", "mode", nextMode], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
|
|
7848
7975
|
} catch (error) {
|
|
7849
7976
|
console.log(`room mode failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7850
7977
|
}
|
|
7851
7978
|
continue;
|
|
7852
7979
|
}
|
|
7853
7980
|
|
|
7981
|
+
if (line === "/room runtime" || line.startsWith("/room runtime ")) {
|
|
7982
|
+
const value = line === "/room runtime" ? "" : line.replace("/room runtime", "").trim();
|
|
7983
|
+
try {
|
|
7984
|
+
await runRoomCommand(value ? ["room", "runtime", value] : ["room", "runtime"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
|
|
7985
|
+
} catch (error) {
|
|
7986
|
+
console.log(`room runtime failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7987
|
+
}
|
|
7988
|
+
continue;
|
|
7989
|
+
}
|
|
7990
|
+
|
|
7854
7991
|
if (line === "/room claim") {
|
|
7855
7992
|
try {
|
|
7856
|
-
await runRoomCommand(["room", "claim"], { cwd: context.cwd, asJson: false });
|
|
7993
|
+
await runRoomCommand(["room", "claim"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
|
|
7857
7994
|
} catch (error) {
|
|
7858
7995
|
console.log(`room claim failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7859
7996
|
}
|
|
@@ -7862,7 +7999,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7862
7999
|
|
|
7863
8000
|
if (line === "/room release") {
|
|
7864
8001
|
try {
|
|
7865
|
-
await runRoomCommand(["room", "release"], { cwd: context.cwd, asJson: false });
|
|
8002
|
+
await runRoomCommand(["room", "release"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
|
|
7866
8003
|
} catch (error) {
|
|
7867
8004
|
console.log(`room release failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7868
8005
|
}
|
|
@@ -9691,7 +9828,7 @@ export async function runCli(argv) {
|
|
|
9691
9828
|
}
|
|
9692
9829
|
|
|
9693
9830
|
if (command === "room") {
|
|
9694
|
-
await runRoomCommand(positional, { cwd: startupCwd, asJson });
|
|
9831
|
+
await runRoomCommand(positional, { cwd: startupCwd, asJson, runtime });
|
|
9695
9832
|
return;
|
|
9696
9833
|
}
|
|
9697
9834
|
|
package/src/gateway-state.js
CHANGED
|
@@ -33,6 +33,7 @@ function normalizeBridgeRequest(parsed = {}) {
|
|
|
33
33
|
username: String(parsed?.username || "").trim(),
|
|
34
34
|
sessionId: String(parsed?.sessionId || "").trim(),
|
|
35
35
|
text: String(parsed?.text || "").trim(),
|
|
36
|
+
runtimeProfile: String(parsed?.runtimeProfile || "").trim(),
|
|
36
37
|
replyToMessageId: Number.isFinite(Number(parsed?.replyToMessageId)) ? Math.floor(Number(parsed.replyToMessageId)) : 0,
|
|
37
38
|
requestedAt: String(parsed?.requestedAt || "").trim(),
|
|
38
39
|
status: String(parsed?.status || "pending").trim() || "pending",
|
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 { addSharedTask, claimSharedOperator, getSharedMember, listSharedTasks, loadSharedProject, moveSharedTask, releaseSharedOperator, setSharedRoom, setSharedRoomMode, upsertSharedMember, removeSharedMember } from "./shared-project.js";
|
|
11
|
+
import { addSharedTask, assignSharedTask, claimSharedOperator, claimSharedTask, getSharedMember, listSharedTasks, loadSharedProject, moveSharedTask, releaseSharedOperator, setSharedRoom, setSharedRoomMode, setSharedRuntimeProfile, 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)), "..");
|
|
@@ -28,6 +28,7 @@ const TELEGRAM_COMMANDS = [
|
|
|
28
28
|
{ command: "room", description: "Show shared room status" },
|
|
29
29
|
{ command: "members", description: "List shared room members" },
|
|
30
30
|
{ command: "tasks", description: "List shared Roundtable tasks" },
|
|
31
|
+
{ command: "room_runtime", description: "Show or set shared room runtime profile" },
|
|
31
32
|
{ command: "mode", description: "Show or set shared room mode" },
|
|
32
33
|
{ command: "claim", description: "Claim operator control for a shared project" },
|
|
33
34
|
{ command: "release", description: "Release operator control for a shared project" },
|
|
@@ -204,7 +205,10 @@ function buildRemoteHelp() {
|
|
|
204
205
|
"<code>/members</code> list shared project members",
|
|
205
206
|
"<code>/tasks</code> list shared project tasks",
|
|
206
207
|
"<code>/task add <text></code> add a shared Roundtable task",
|
|
208
|
+
"<code>/task assign <id> <member-id></code> assign a shared task",
|
|
209
|
+
"<code>/task claim <id></code> claim a shared task for yourself",
|
|
207
210
|
"<code>/task move <id> <open|active|blocked|done></code> move a shared Roundtable task",
|
|
211
|
+
"<code>/room-runtime</code> or <code>/room-runtime <name|clear></code> inspect or set the shared room runtime profile",
|
|
208
212
|
"<code>/invite <user-id> [owner|editor|observer]</code> add or update a shared project member",
|
|
209
213
|
"<code>/remove-member <user-id></code> remove a shared project member",
|
|
210
214
|
"<code>/mode</code> or <code>/mode <chat|plan|execute></code> inspect or change shared room mode",
|
|
@@ -274,7 +278,7 @@ function formatSessionListMarkup(currentSessionId, sessions = []) {
|
|
|
274
278
|
return lines.join("\n");
|
|
275
279
|
}
|
|
276
280
|
|
|
277
|
-
function formatTelegramRoomMarkup(project) {
|
|
281
|
+
function formatTelegramRoomMarkup(project, options = {}) {
|
|
278
282
|
if (!project?.enabled) {
|
|
279
283
|
return "<b>Shared room</b>\nThis project is not shared.";
|
|
280
284
|
}
|
|
@@ -288,6 +292,21 @@ function formatTelegramRoomMarkup(project) {
|
|
|
288
292
|
const memberLines = members.length
|
|
289
293
|
? members.map((member) => `• ${escapeTelegramHtml(member.name || member.id)} <i>(${escapeTelegramHtml(member.role || "editor")})</i>`).join("\n")
|
|
290
294
|
: "• none";
|
|
295
|
+
const executor = options.executor || {};
|
|
296
|
+
const executorBits = [
|
|
297
|
+
`surface: <code>${escapeTelegramHtml(executor.surface || "telegram")}</code>`,
|
|
298
|
+
`provider: <code>${escapeTelegramHtml(executor.provider || "unknown")}</code>`,
|
|
299
|
+
`model: <code>${escapeTelegramHtml(executor.model || "unknown")}</code>`
|
|
300
|
+
];
|
|
301
|
+
if (project.runtimeProfile) {
|
|
302
|
+
executorBits.unshift(`room runtime: <code>${escapeTelegramHtml(project.runtimeProfile)}</code>`);
|
|
303
|
+
}
|
|
304
|
+
if (executor.runtimeProfile) {
|
|
305
|
+
executorBits.push(`runtime profile: <code>${escapeTelegramHtml(executor.runtimeProfile)}</code>`);
|
|
306
|
+
}
|
|
307
|
+
if (executor.hostSessionId) {
|
|
308
|
+
executorBits.push(`host session: <code>${escapeTelegramHtml(executor.hostSessionId)}</code>`);
|
|
309
|
+
}
|
|
291
310
|
return [
|
|
292
311
|
"<b>Shared room</b>",
|
|
293
312
|
`project: <code>${escapeTelegramHtml(project.projectName || "")}</code>`,
|
|
@@ -295,6 +314,8 @@ function formatTelegramRoomMarkup(project) {
|
|
|
295
314
|
`room mode: <code>${escapeTelegramHtml(project.roomMode || "chat")}</code>`,
|
|
296
315
|
`room: <code>${escapeTelegramHtml(roomLabel)}</code>`,
|
|
297
316
|
`active operator: <code>${escapeTelegramHtml(active)}</code>`,
|
|
317
|
+
"<b>Executor</b>",
|
|
318
|
+
...executorBits,
|
|
298
319
|
"<b>Members</b>",
|
|
299
320
|
memberLines
|
|
300
321
|
].join("\n");
|
|
@@ -320,10 +341,48 @@ function formatTelegramTasksMarkup(tasks = []) {
|
|
|
320
341
|
}
|
|
321
342
|
return [
|
|
322
343
|
"<b>Shared tasks</b>",
|
|
323
|
-
...tasks.map((task) => `• <code>${escapeTelegramHtml(task.id)}</code> <i>(${escapeTelegramHtml(task.state)})</i> ${escapeTelegramHtml(task.text)}`)
|
|
344
|
+
...tasks.map((task) => `• <code>${escapeTelegramHtml(task.id)}</code> <i>(${escapeTelegramHtml(task.state)})</i> ${escapeTelegramHtml(task.text)}${task.assignedTo ? ` <code>@${escapeTelegramHtml(task.assignedTo)}</code>` : ""}`)
|
|
324
345
|
].join("\n");
|
|
325
346
|
}
|
|
326
347
|
|
|
348
|
+
function classifyTelegramGroupIntent(text = "") {
|
|
349
|
+
const normalized = String(text || "").trim();
|
|
350
|
+
const lower = normalized.toLowerCase();
|
|
351
|
+
if (!normalized) return { kind: "chat", reason: "empty" };
|
|
352
|
+
if (/^(hi|hello|hey|yo|thanks|thank you|cool|nice|ok|okay|k)\b/.test(lower)) {
|
|
353
|
+
return { kind: "chat", reason: "casual language" };
|
|
354
|
+
}
|
|
355
|
+
if (
|
|
356
|
+
lower.includes("brainstorm")
|
|
357
|
+
|| lower.includes("what do you think")
|
|
358
|
+
|| lower.includes("should we")
|
|
359
|
+
|| lower.includes("could we")
|
|
360
|
+
|| lower.includes("let's think")
|
|
361
|
+
|| lower.includes("plan this")
|
|
362
|
+
|| lower.includes("ideas for")
|
|
363
|
+
|| normalized.endsWith("?")
|
|
364
|
+
) {
|
|
365
|
+
return { kind: "plan", reason: "planning language" };
|
|
366
|
+
}
|
|
367
|
+
if (
|
|
368
|
+
/^(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)
|
|
369
|
+
|| /[`/].+/.test(normalized)
|
|
370
|
+
|| /\b(file|folder|directory|repo|project|code|command|function|component|page|route|api|schema|bug)\b/.test(lower)
|
|
371
|
+
) {
|
|
372
|
+
return { kind: "execution", reason: "explicit work request" };
|
|
373
|
+
}
|
|
374
|
+
return { kind: "chat", reason: "default non-execution" };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function suggestTaskText(text = "") {
|
|
378
|
+
return String(text || "")
|
|
379
|
+
.replace(/^@[\w_]+\s*/g, "")
|
|
380
|
+
.replace(/^(should we|could we|what do you think about|brainstorm|plan)\s+/i, "")
|
|
381
|
+
.replace(/[?]+$/g, "")
|
|
382
|
+
.trim()
|
|
383
|
+
.slice(0, 180);
|
|
384
|
+
}
|
|
385
|
+
|
|
327
386
|
function parseInviteCommand(text) {
|
|
328
387
|
const parts = String(text || "").trim().split(/\s+/).filter(Boolean);
|
|
329
388
|
const userId = String(parts[1] || "").trim();
|
|
@@ -696,6 +755,19 @@ class TelegramGateway {
|
|
|
696
755
|
return { session, project: next };
|
|
697
756
|
}
|
|
698
757
|
|
|
758
|
+
async buildRoomMarkup(message, sessionId) {
|
|
759
|
+
const { project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
760
|
+
const host = await this.getLiveBridgeHost();
|
|
761
|
+
const executor = {
|
|
762
|
+
surface: host ? "live-tui" : "telegram-fallback",
|
|
763
|
+
provider: this.runtime.provider,
|
|
764
|
+
model: this.runtime.model,
|
|
765
|
+
runtimeProfile: project?.runtimeProfile || this.channel.defaultRuntimeProfile || this.gateway.defaultRuntimeProfile || "",
|
|
766
|
+
hostSessionId: host?.sessionId || ""
|
|
767
|
+
};
|
|
768
|
+
return project?.enabled ? formatTelegramRoomMarkup(project, { executor }) : "<b>Shared room</b>\nThis project is not shared.";
|
|
769
|
+
}
|
|
770
|
+
|
|
699
771
|
async ensureSharedOperator(message, sessionId) {
|
|
700
772
|
const { session, project } = await this.loadSharedProjectForSession(sessionId);
|
|
701
773
|
if (!project?.enabled) return { ok: true, project: null, session };
|
|
@@ -750,6 +822,7 @@ class TelegramGateway {
|
|
|
750
822
|
}
|
|
751
823
|
|
|
752
824
|
const requestId = createBridgeRequestId();
|
|
825
|
+
const { project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
753
826
|
const bridge = await loadGatewayBridge("telegram");
|
|
754
827
|
const requests = Array.isArray(bridge.pendingRequests) ? bridge.pendingRequests : [];
|
|
755
828
|
requests.push({
|
|
@@ -759,6 +832,7 @@ class TelegramGateway {
|
|
|
759
832
|
username: [message?.from?.username, message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim(),
|
|
760
833
|
sessionId: String(sessionId || "").trim(),
|
|
761
834
|
text: String(promptText || "").trim(),
|
|
835
|
+
runtimeProfile: String(project?.runtimeProfile || "").trim(),
|
|
762
836
|
replyToMessageId: message.message_id,
|
|
763
837
|
requestedAt: new Date().toISOString(),
|
|
764
838
|
status: "pending",
|
|
@@ -819,10 +893,25 @@ class TelegramGateway {
|
|
|
819
893
|
}
|
|
820
894
|
|
|
821
895
|
async runPromptFallback(sessionId, promptText) {
|
|
896
|
+
let providerArgs = [];
|
|
897
|
+
try {
|
|
898
|
+
const session = await loadSession(sessionId);
|
|
899
|
+
const project = await loadSharedProject(session.cwd || this.cwd);
|
|
900
|
+
const profileName = String(project?.runtimeProfile || "").trim();
|
|
901
|
+
const profile = profileName ? this.runtime.runtimeProfiles?.[profileName] : null;
|
|
902
|
+
if (profile) {
|
|
903
|
+
if (profile.provider) providerArgs.push("--provider", String(profile.provider));
|
|
904
|
+
if (profile.model) providerArgs.push("--model", String(profile.model));
|
|
905
|
+
if (profile.designModel) providerArgs.push("--design-model", String(profile.designModel));
|
|
906
|
+
if (profile.baseUrl) providerArgs.push("--base-url", String(profile.baseUrl));
|
|
907
|
+
}
|
|
908
|
+
} catch {}
|
|
909
|
+
|
|
822
910
|
const { stdout } = await this.runWaterbrother([
|
|
823
911
|
"resume",
|
|
824
912
|
sessionId,
|
|
825
913
|
promptText,
|
|
914
|
+
...providerArgs,
|
|
826
915
|
"--skip-onboarding",
|
|
827
916
|
"--approval",
|
|
828
917
|
"never",
|
|
@@ -953,8 +1042,7 @@ class TelegramGateway {
|
|
|
953
1042
|
}
|
|
954
1043
|
|
|
955
1044
|
if (text === "/room") {
|
|
956
|
-
|
|
957
|
-
await this.sendMessage(message.chat.id, project?.enabled ? formatTelegramRoomMarkup(project) : "<b>Shared room</b>\nThis project is not shared.", message.message_id);
|
|
1045
|
+
await this.sendMessage(message.chat.id, await this.buildRoomMarkup(message, sessionId), message.message_id);
|
|
958
1046
|
return;
|
|
959
1047
|
}
|
|
960
1048
|
|
|
@@ -975,6 +1063,26 @@ class TelegramGateway {
|
|
|
975
1063
|
return;
|
|
976
1064
|
}
|
|
977
1065
|
|
|
1066
|
+
if (text.startsWith("/task ")) {
|
|
1067
|
+
const taskShortcut = text.replace("/task", "").trim().toLowerCase();
|
|
1068
|
+
if (taskShortcut === "") {
|
|
1069
|
+
await this.sendMessage(message.chat.id, "Usage: /task add <text> | /task assign <id> <member-id> | /task claim <id> | /task move <id> <open|active|blocked|done>", message.message_id);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (text === "/room-runtime") {
|
|
1075
|
+
const { project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1076
|
+
await this.sendMessage(
|
|
1077
|
+
message.chat.id,
|
|
1078
|
+
project?.enabled
|
|
1079
|
+
? `<b>Shared room runtime profile</b>\n<code>${escapeTelegramHtml(project.runtimeProfile || "none")}</code>`
|
|
1080
|
+
: "This project is not shared.",
|
|
1081
|
+
message.message_id
|
|
1082
|
+
);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
978
1086
|
if (text === "/mode") {
|
|
979
1087
|
const { project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
980
1088
|
await this.sendMessage(
|
|
@@ -1003,6 +1111,29 @@ class TelegramGateway {
|
|
|
1003
1111
|
return;
|
|
1004
1112
|
}
|
|
1005
1113
|
|
|
1114
|
+
if (text.startsWith("/room-runtime ")) {
|
|
1115
|
+
const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1116
|
+
if (!project?.enabled) {
|
|
1117
|
+
await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const requestedProfile = text.replace("/room-runtime", "").trim();
|
|
1121
|
+
const normalized = requestedProfile.toLowerCase() === "clear" ? "" : requestedProfile;
|
|
1122
|
+
try {
|
|
1123
|
+
const next = await setSharedRuntimeProfile(session.cwd || this.cwd, normalized, { actorId: userId });
|
|
1124
|
+
await this.sendMessage(
|
|
1125
|
+
message.chat.id,
|
|
1126
|
+
next.runtimeProfile
|
|
1127
|
+
? `Shared room runtime profile set to <code>${escapeTelegramHtml(next.runtimeProfile)}</code>`
|
|
1128
|
+
: "Shared room runtime profile cleared.",
|
|
1129
|
+
message.message_id
|
|
1130
|
+
);
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
|
|
1133
|
+
}
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1006
1137
|
if (text === "/claim") {
|
|
1007
1138
|
const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1008
1139
|
if (!project?.enabled) {
|
|
@@ -1029,7 +1160,7 @@ class TelegramGateway {
|
|
|
1029
1160
|
}
|
|
1030
1161
|
try {
|
|
1031
1162
|
const released = await releaseSharedOperator(session.cwd || this.cwd, userId, { actorId: userId });
|
|
1032
|
-
await this.sendMessage(message.chat.id, released.activeOperator?.id ?
|
|
1163
|
+
await this.sendMessage(message.chat.id, released.activeOperator?.id ? await this.buildRoomMarkup(message, sessionId) : "Shared room released.", message.message_id);
|
|
1033
1164
|
} catch (error) {
|
|
1034
1165
|
await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
|
|
1035
1166
|
}
|
|
@@ -1129,6 +1260,46 @@ class TelegramGateway {
|
|
|
1129
1260
|
return;
|
|
1130
1261
|
}
|
|
1131
1262
|
|
|
1263
|
+
if (text.startsWith("/task assign ")) {
|
|
1264
|
+
const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1265
|
+
if (!project?.enabled) {
|
|
1266
|
+
await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
const [taskId, memberId] = text.replace("/task assign", "").trim().split(/\s+/, 2);
|
|
1270
|
+
if (!taskId || !memberId) {
|
|
1271
|
+
await this.sendMessage(message.chat.id, "Usage: /task assign <id> <member-id>", message.message_id);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
try {
|
|
1275
|
+
const result = await assignSharedTask(session.cwd || this.cwd, taskId, memberId, { actorId: userId });
|
|
1276
|
+
await this.sendMessage(message.chat.id, `Assigned shared task <code>${escapeTelegramHtml(result.task.id)}</code> to <code>${escapeTelegramHtml(memberId)}</code>`, message.message_id);
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
|
|
1279
|
+
}
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (text.startsWith("/task claim ")) {
|
|
1284
|
+
const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1285
|
+
if (!project?.enabled) {
|
|
1286
|
+
await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
const taskId = text.replace("/task claim", "").trim();
|
|
1290
|
+
if (!taskId) {
|
|
1291
|
+
await this.sendMessage(message.chat.id, "Usage: /task claim <id>", message.message_id);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
try {
|
|
1295
|
+
const result = await claimSharedTask(session.cwd || this.cwd, taskId, { actorId: userId });
|
|
1296
|
+
await this.sendMessage(message.chat.id, `Claimed shared task <code>${escapeTelegramHtml(result.task.id)}</code>`, message.message_id);
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
|
|
1299
|
+
}
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1132
1303
|
if (text === "/runtime") {
|
|
1133
1304
|
const status = await this.runRuntimeStatus();
|
|
1134
1305
|
await this.sendMessage(message.chat.id, formatRuntimeStatus(status), message.message_id);
|
|
@@ -1236,16 +1407,48 @@ class TelegramGateway {
|
|
|
1236
1407
|
return;
|
|
1237
1408
|
}
|
|
1238
1409
|
|
|
1410
|
+
const promptText = this.stripBotMention(text);
|
|
1411
|
+
const groupIntent = this.isGroupChat(message) ? classifyTelegramGroupIntent(promptText) : { kind: "execution", reason: "private chat" };
|
|
1412
|
+
const sharedBinding = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1413
|
+
if (
|
|
1414
|
+
this.isGroupChat(message)
|
|
1415
|
+
&& sharedBinding.project?.enabled
|
|
1416
|
+
&& sharedBinding.project.roomMode === "plan"
|
|
1417
|
+
&& /^(task:|todo:)/i.test(promptText)
|
|
1418
|
+
) {
|
|
1419
|
+
const taskText = promptText.replace(/^(task:|todo:)\s*/i, "").trim();
|
|
1420
|
+
if (taskText) {
|
|
1421
|
+
try {
|
|
1422
|
+
const result = await addSharedTask(sharedBinding.session.cwd || this.cwd, taskText, { actorId: userId });
|
|
1423
|
+
await this.sendMessage(message.chat.id, `Planner captured task <code>${escapeTelegramHtml(result.task.id)}</code>`, message.message_id);
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
|
|
1426
|
+
}
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
if (this.isGroupChat(message) && groupIntent.kind !== "execution") {
|
|
1431
|
+
const suggestion = suggestTaskText(promptText);
|
|
1432
|
+
const planHint = groupIntent.kind === "plan"
|
|
1433
|
+
? `Planning/brainstorming message detected. No execution will run in the group. Suggested task: ${suggestion || "capture the next concrete step"}. Use /task add ${suggestion || "<text>"} or switch to /mode execute when you want Waterbrother to act.`
|
|
1434
|
+
: "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.";
|
|
1435
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(planHint), message.message_id, { parseMode: "HTML" });
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1239
1439
|
const stopTyping = await this.startTypingLoop(message.chat.id);
|
|
1240
1440
|
let previewMessage = null;
|
|
1241
1441
|
try {
|
|
1242
1442
|
const operatorGate = await this.ensureSharedOperator(message, sessionId);
|
|
1243
1443
|
if (!operatorGate.ok) {
|
|
1244
|
-
|
|
1444
|
+
const gateReason =
|
|
1445
|
+
this.isGroupChat(message) && groupIntent.kind === "execution" && operatorGate.project?.enabled && operatorGate.project?.roomMode !== "execute"
|
|
1446
|
+
? `Execution request detected, but the shared room is in ${operatorGate.project?.roomMode || "chat"} mode. Switch to /mode execute before asking Waterbrother to change code.`
|
|
1447
|
+
: operatorGate.reason || "Shared room is not available.";
|
|
1448
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(gateReason), message.message_id, { parseMode: "HTML" });
|
|
1245
1449
|
return;
|
|
1246
1450
|
}
|
|
1247
1451
|
previewMessage = await this.sendProgressMessage(message.chat.id, message.message_id);
|
|
1248
|
-
const promptText = this.stripBotMention(text);
|
|
1249
1452
|
const content = (await this.runPromptViaBridge(message, sessionId, promptText))
|
|
1250
1453
|
?? (await this.runPromptFallback(sessionId, promptText));
|
|
1251
1454
|
await this.deliverPromptResult(message.chat.id, message.message_id, previewMessage, content);
|
package/src/shared-project.js
CHANGED
|
@@ -54,6 +54,7 @@ function normalizeSharedProject(project = {}, cwd = process.cwd()) {
|
|
|
54
54
|
roomMode: ["chat", "plan", "execute"].includes(String(project.roomMode || "").trim())
|
|
55
55
|
? String(project.roomMode).trim()
|
|
56
56
|
: "chat",
|
|
57
|
+
runtimeProfile: String(project.runtimeProfile || "").trim(),
|
|
57
58
|
members,
|
|
58
59
|
tasks,
|
|
59
60
|
activeOperator: activeOperator?.id ? activeOperator : null,
|
|
@@ -112,7 +113,7 @@ function defaultRoundtableContent(project) {
|
|
|
112
113
|
const taskLines = TASK_STATES.flatMap((state) => {
|
|
113
114
|
const tasks = (project.tasks || []).filter((task) => task.state === state);
|
|
114
115
|
if (!tasks.length) return [`- ${state}: -`];
|
|
115
|
-
return [`- ${state}:`, ...tasks.map((task) => ` - [${task.id}] ${task.text}`)];
|
|
116
|
+
return [`- ${state}:`, ...tasks.map((task) => ` - [${task.id}] ${task.text}${task.assignedTo ? ` @${task.assignedTo}` : ""}`)];
|
|
116
117
|
});
|
|
117
118
|
return [
|
|
118
119
|
"# Roundtable",
|
|
@@ -121,6 +122,7 @@ function defaultRoundtableContent(project) {
|
|
|
121
122
|
`- Name: ${project.projectName}`,
|
|
122
123
|
`- Mode: ${project.mode}`,
|
|
123
124
|
`- Room mode: ${project.roomMode}`,
|
|
125
|
+
`- Runtime profile: ${project.runtimeProfile || "-"}`,
|
|
124
126
|
`- Approval policy: ${project.approvalPolicy}`,
|
|
125
127
|
project.room?.provider ? `- Room: ${project.room.provider} ${project.room.chatId || ""}`.trim() : "- Room: not linked",
|
|
126
128
|
"",
|
|
@@ -149,7 +151,7 @@ function buildRoundtableTaskSection(project) {
|
|
|
149
151
|
const taskLines = TASK_STATES.flatMap((state) => {
|
|
150
152
|
const tasks = (project.tasks || []).filter((task) => task.state === state);
|
|
151
153
|
if (!tasks.length) return [`- ${state}: -`];
|
|
152
|
-
return [`- ${state}:`, ...tasks.map((task) => ` - [${task.id}] ${task.text}`)];
|
|
154
|
+
return [`- ${state}:`, ...tasks.map((task) => ` - [${task.id}] ${task.text}${task.assignedTo ? ` @${task.assignedTo}` : ""}`)];
|
|
153
155
|
});
|
|
154
156
|
return ["## Task Queue", ...taskLines, ""].join("\n");
|
|
155
157
|
}
|
|
@@ -269,6 +271,21 @@ export async function setSharedRoomMode(cwd, roomMode = "chat", options = {}) {
|
|
|
269
271
|
return next;
|
|
270
272
|
}
|
|
271
273
|
|
|
274
|
+
export async function setSharedRuntimeProfile(cwd, runtimeProfile = "", options = {}) {
|
|
275
|
+
const existing = await loadSharedProject(cwd);
|
|
276
|
+
requireOwner(existing, options.actorId);
|
|
277
|
+
const normalized = String(runtimeProfile || "").trim();
|
|
278
|
+
const next = await saveSharedProject(cwd, {
|
|
279
|
+
...existing,
|
|
280
|
+
runtimeProfile: normalized
|
|
281
|
+
});
|
|
282
|
+
await appendRoundtableEvent(
|
|
283
|
+
cwd,
|
|
284
|
+
`- ${new Date().toISOString()}: room runtime profile ${normalized ? `set to ${normalized}` : "cleared"}`
|
|
285
|
+
);
|
|
286
|
+
return next;
|
|
287
|
+
}
|
|
288
|
+
|
|
272
289
|
export function getSharedMember(project, memberId = "") {
|
|
273
290
|
const normalizedId = String(memberId || "").trim();
|
|
274
291
|
if (!normalizedId || !project?.members?.length) return null;
|
|
@@ -409,6 +426,55 @@ export async function moveSharedTask(cwd, taskId = "", state = "open", options =
|
|
|
409
426
|
return { project: next, task: tasks[index] };
|
|
410
427
|
}
|
|
411
428
|
|
|
429
|
+
export async function assignSharedTask(cwd, taskId = "", memberId = "", options = {}) {
|
|
430
|
+
const existing = await loadSharedProject(cwd);
|
|
431
|
+
requireEditor(existing, options.actorId);
|
|
432
|
+
const normalizedId = String(taskId || "").trim();
|
|
433
|
+
const normalizedMemberId = String(memberId || "").trim();
|
|
434
|
+
if (!normalizedId) throw new Error("task id is required");
|
|
435
|
+
if (!normalizedMemberId) throw new Error("member id is required");
|
|
436
|
+
if (!getSharedMember(existing, normalizedMemberId)) {
|
|
437
|
+
throw new Error(`No shared-project member found for ${normalizedMemberId}`);
|
|
438
|
+
}
|
|
439
|
+
const tasks = [...(existing.tasks || [])];
|
|
440
|
+
const index = tasks.findIndex((task) => task.id === normalizedId);
|
|
441
|
+
if (index < 0) throw new Error(`No shared task found for ${normalizedId}`);
|
|
442
|
+
tasks[index] = {
|
|
443
|
+
...tasks[index],
|
|
444
|
+
assignedTo: normalizedMemberId,
|
|
445
|
+
updatedAt: new Date().toISOString()
|
|
446
|
+
};
|
|
447
|
+
const next = await saveSharedProject(cwd, {
|
|
448
|
+
...existing,
|
|
449
|
+
tasks
|
|
450
|
+
});
|
|
451
|
+
await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: task assigned [${tasks[index].id}] -> ${normalizedMemberId}`);
|
|
452
|
+
return { project: next, task: tasks[index] };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export async function claimSharedTask(cwd, taskId = "", options = {}) {
|
|
456
|
+
const existing = await loadSharedProject(cwd);
|
|
457
|
+
requireMember(existing, options.actorId);
|
|
458
|
+
const normalizedId = String(taskId || "").trim();
|
|
459
|
+
const actorId = String(options.actorId || "").trim();
|
|
460
|
+
if (!normalizedId) throw new Error("task id is required");
|
|
461
|
+
const tasks = [...(existing.tasks || [])];
|
|
462
|
+
const index = tasks.findIndex((task) => task.id === normalizedId);
|
|
463
|
+
if (index < 0) throw new Error(`No shared task found for ${normalizedId}`);
|
|
464
|
+
tasks[index] = {
|
|
465
|
+
...tasks[index],
|
|
466
|
+
assignedTo: actorId,
|
|
467
|
+
state: "active",
|
|
468
|
+
updatedAt: new Date().toISOString()
|
|
469
|
+
};
|
|
470
|
+
const next = await saveSharedProject(cwd, {
|
|
471
|
+
...existing,
|
|
472
|
+
tasks
|
|
473
|
+
});
|
|
474
|
+
await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: task claimed [${tasks[index].id}] by ${actorId}`);
|
|
475
|
+
return { project: next, task: tasks[index] };
|
|
476
|
+
}
|
|
477
|
+
|
|
412
478
|
export async function claimSharedOperator(cwd, operator = {}) {
|
|
413
479
|
const existing = await loadSharedProject(cwd);
|
|
414
480
|
requireSharedProject(existing);
|