jeo-code 0.11.2 → 0.11.4
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/CHANGELOG.md +51 -1
- package/README.ja.md +62 -33
- package/README.ko.md +65 -35
- package/README.md +61 -32
- package/README.zh.md +64 -35
- package/package.json +1 -1
- package/src/agent/config-schema.ts +20 -7
- package/src/agent/debug-session.ts +7 -3
- package/src/agent/engine.ts +46 -102
- package/src/agent/loop-guards.ts +11 -13
- package/src/agent/notify/daemon-control.ts +109 -24
- package/src/agent/notify/discord-api.ts +148 -0
- package/src/agent/notify/discord-daemon.ts +443 -0
- package/src/agent/notify/paths.ts +1 -1
- package/src/agent/notify/session-discovery.ts +16 -0
- package/src/agent/notify/session-endpoint.ts +43 -25
- package/src/agent/notify/slack-api.ts +177 -0
- package/src/agent/notify/slack-daemon.ts +361 -0
- package/src/agent/notify/telegram-daemon.ts +215 -83
- package/src/agent/state.ts +13 -1
- package/src/ai/model-discovery.ts +13 -21
- package/src/autopilot.ts +14 -5
- package/src/cli/runner.ts +4 -4
- package/src/commands/approve.ts +21 -16
- package/src/commands/daemon.ts +5 -3
- package/src/commands/launch/agents-slash.ts +8 -3
- package/src/commands/launch.ts +36 -44
- package/src/commands/notify.ts +215 -83
- package/src/commands/ralplan.ts +2 -1
- package/src/commands/team.ts +17 -12
- package/src/commands/ultragoal.ts +2 -1
package/src/commands/notify.ts
CHANGED
|
@@ -1,109 +1,241 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `jeo notify setup|status` — configure and inspect the remote subagent
|
|
3
|
-
* notification/control channel (gjc `gjc notify setup`/`gjc notify status`
|
|
4
|
-
* parity, scoped to jeo's Telegram + subagent surface; see `src/agent/notify/`).
|
|
5
|
-
*/
|
|
1
|
+
/** Configure and inspect Telegram, Discord, and Slack notification channels. */
|
|
6
2
|
import { createInterface } from "node:readline/promises";
|
|
7
3
|
import { readGlobalConfig, saveConfigPatch } from "../agent/state";
|
|
8
|
-
import { TelegramApi
|
|
9
|
-
import {
|
|
4
|
+
import { TelegramApi } from "../agent/notify/telegram-api";
|
|
5
|
+
import { DiscordApi, DiscordApiError } from "../agent/notify/discord-api";
|
|
6
|
+
import { SlackApi, SlackApiError } from "../agent/notify/slack-api";
|
|
7
|
+
import { acquireDaemonLock, daemonStatus } from "../agent/notify/daemon-control";
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
type Provider = "telegram" | "discord" | "slack";
|
|
10
|
+
interface NotifyOptions {
|
|
11
|
+
fetchImpl?: typeof fetch;
|
|
12
|
+
prompt?: (question: string) => Promise<string>;
|
|
13
|
+
pairingChallenge?: () => string;
|
|
14
|
+
now?: () => number;
|
|
15
|
+
}
|
|
16
|
+
interface Flags {
|
|
17
|
+
provider: Provider;
|
|
18
|
+
token?: string;
|
|
19
|
+
tokenEnv?: string;
|
|
20
|
+
appTokenEnv?: string;
|
|
21
|
+
chatId?: string;
|
|
22
|
+
channelId?: string;
|
|
23
|
+
allowedUserIds?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseFlags(args: string[]): Flags {
|
|
27
|
+
const out: Flags = { provider: "telegram" };
|
|
13
28
|
for (let i = 0; i < args.length; i++) {
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
const flag = args[i];
|
|
30
|
+
const value = args[++i];
|
|
31
|
+
if (!value || value.startsWith("--")) throw new Error("Missing value for notify option (use --token-env NAME for credentials)");
|
|
32
|
+
if (flag === "--provider" || flag === "--platform") {
|
|
33
|
+
if (value !== "telegram" && value !== "discord" && value !== "slack") throw new Error("Provider must be telegram, discord or slack");
|
|
34
|
+
out.provider = value;
|
|
35
|
+
} else if (flag === "--token") out.token = value;
|
|
36
|
+
else if (flag === "--token-env") out.tokenEnv = value;
|
|
37
|
+
else if (flag === "--app-token-env") out.appTokenEnv = value;
|
|
38
|
+
else if (flag === "--chat-id") out.chatId = value;
|
|
39
|
+
else if (flag === "--channel-id") out.channelId = value;
|
|
40
|
+
else if (flag === "--allowed-user-ids") {
|
|
41
|
+
out.allowedUserIds = [...new Set(value.split(",").map(id => id.trim()))];
|
|
42
|
+
} else throw new Error("Unknown notify option; use --provider, --token-env, --app-token-env, --chat-id, --channel-id or --allowed-user-ids");
|
|
43
|
+
}
|
|
44
|
+
if (out.chatId && !/^-?[1-9]\d*$/.test(out.chatId)) throw new Error("Telegram chat ID must be a nonzero decimal ID");
|
|
45
|
+
if (out.provider === "slack") {
|
|
46
|
+
if (out.channelId && !/^[CGD][A-Z0-9]+$/.test(out.channelId)) throw new Error("Slack channel ID must start with C, G or D and contain uppercase letters and digits");
|
|
47
|
+
if (out.allowedUserIds && !out.allowedUserIds.every(id => /^[UW][A-Z0-9]+$/.test(id))) throw new Error("Slack allowed user IDs must start with U or W and contain uppercase letters and digits");
|
|
48
|
+
} else {
|
|
49
|
+
if (out.channelId && !/^[1-9]\d{0,19}$/.test(out.channelId)) throw new Error("Discord channel ID must be a positive decimal ID of at most 20 digits");
|
|
50
|
+
if (out.allowedUserIds && !out.allowedUserIds.every(id => /^[1-9]\d{0,19}$/.test(id))) throw new Error("Allowed user IDs must be positive decimal IDs (up to 20 digits) separated by commas");
|
|
16
51
|
}
|
|
17
52
|
return out;
|
|
18
53
|
}
|
|
19
54
|
|
|
20
|
-
async function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
55
|
+
async function question(text: string, opts: NotifyOptions): Promise<string> {
|
|
56
|
+
if (opts.prompt) return (await opts.prompt(text)).trim();
|
|
57
|
+
if (!process.stdin.isTTY) throw new Error("Missing setup arguments; use --token-env and explicit destination/user IDs without a TTY");
|
|
58
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
59
|
+
try { return (await rl.question(text)).trim(); } finally { rl.close(); }
|
|
60
|
+
}
|
|
24
61
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
62
|
+
// Telegram puts credentials in request URLs. Never print underlying fetch errors.
|
|
63
|
+
function boundedFetch(opts: NotifyOptions): typeof fetch {
|
|
64
|
+
return ((input, init) => (opts.fetchImpl ?? fetch)(input, { ...init, signal: init?.signal ?? AbortSignal.timeout(15_000) })) as typeof fetch;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function verifySlack(api: SlackApi, channelId: string): Promise<void> {
|
|
68
|
+
const me = await api.getMe();
|
|
69
|
+
const channel = await api.getChannel(channelId);
|
|
70
|
+
if (channel.id !== channelId || (channel.context_team_id && channel.context_team_id !== me.team_id)) throw new Error("Slack bot and destination workspace validation failed");
|
|
71
|
+
await api.openConnection();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function runSetup(flags: Flags, opts: NotifyOptions): Promise<void> {
|
|
75
|
+
const envName = flags.tokenEnv ?? `JEO_${flags.provider.toUpperCase()}_BOT_TOKEN`;
|
|
76
|
+
let token = flags.token ?? process.env[envName]?.trim();
|
|
77
|
+
if (!token) {
|
|
78
|
+
const name = await question(`Set the bot token in an environment variable, then enter its NAME (default ${envName}): `, opts);
|
|
79
|
+
token = process.env[name || envName]?.trim();
|
|
80
|
+
}
|
|
81
|
+
if (!token) throw new Error(`Bot token environment variable is empty; use --token-env NAME (preferred over --token)`);
|
|
82
|
+
const existing = (await readGlobalConfig()).notifications;
|
|
83
|
+
let allowedUserIds = flags.allowedUserIds;
|
|
84
|
+
if (flags.provider === "slack") {
|
|
85
|
+
const appEnvName = flags.appTokenEnv ?? "SLACK_APP_TOKEN";
|
|
86
|
+
let appToken = process.env[appEnvName]?.trim();
|
|
87
|
+
if (!appToken) {
|
|
88
|
+
const name = await question(`Set the Slack app token in an environment variable, then enter its NAME (default ${appEnvName}): `, opts);
|
|
89
|
+
appToken = process.env[name || appEnvName]?.trim();
|
|
32
90
|
}
|
|
33
|
-
|
|
91
|
+
if (!appToken) throw new Error("Slack app token environment variable is empty; use --app-token-env NAME");
|
|
92
|
+
const channelId = flags.channelId ?? existing?.slack?.channelId ?? await question("Slack channel ID: ", opts);
|
|
93
|
+
if (!/^[CGD][A-Z0-9]+$/.test(channelId)) throw new Error("Slack requires a valid channel ID");
|
|
94
|
+
const sameDestination = existing?.slack?.botToken === token && existing.slack.appToken === appToken && existing.slack.channelId === channelId;
|
|
95
|
+
if (!allowedUserIds?.length && (!sameDestination || !existing?.slack?.allowedUserIds.length)) allowedUserIds = (await question("Allowed Slack human user IDs (comma separated; required after token/destination changes): ", opts)).split(",").map(id => id.trim());
|
|
96
|
+
const api = new SlackApi(token, appToken, { fetchImpl: boundedFetch(opts) });
|
|
34
97
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
98
|
+
await verifySlack(api, channelId);
|
|
99
|
+
await saveConfigPatch(raw => {
|
|
100
|
+
const previous = raw.notifications?.slack;
|
|
101
|
+
const unchanged = previous?.botToken === token && previous.appToken === appToken && previous.channelId === channelId;
|
|
102
|
+
const owners = allowedUserIds ?? (unchanged ? previous.allowedUserIds : undefined);
|
|
103
|
+
if (!owners?.length || !owners.every(id => /^[UW][A-Z0-9]+$/.test(id))) throw new Error("Slack requires explicit allowed human user IDs; repeat --allowed-user-ids after token or destination changes");
|
|
104
|
+
return { notifications: { ...raw.notifications, enabled: true, slack: { botToken: token, appToken, channelId, allowedUserIds: owners } } };
|
|
105
|
+
});
|
|
106
|
+
} finally { api.stop(); }
|
|
107
|
+
console.log(`Slack bot and channel verified; channelId=${channelId}. Notifications enabled. Setup did not send a message.`);
|
|
108
|
+
console.log("Socket Mode URL verified; app-token ownership and socket connectivity are checked by the daemon handshake. Token rotation requires an explicit user allowlist.");
|
|
109
|
+
} else if (flags.provider === "discord") {
|
|
110
|
+
const channelId = flags.channelId ?? existing?.discord?.channelId ?? await question("Discord channel ID: ", opts);
|
|
111
|
+
const sameDestination = existing?.discord?.botToken === token && existing.discord.channelId === channelId;
|
|
112
|
+
if (!allowedUserIds?.length && (!sameDestination || !existing?.discord?.allowedUserIds?.length)) allowedUserIds = (await question("Allowed Discord user IDs (comma separated; required after token/destination changes): ", opts)).split(",").map(id => id.trim());
|
|
113
|
+
if (!/^[1-9]\d{0,19}$/.test(channelId)) throw new Error("Discord requires a positive channel ID (up to 20 digits)");
|
|
114
|
+
const api = new DiscordApi(token, { fetchImpl: boundedFetch(opts) });
|
|
115
|
+
const me = await api.getMe();
|
|
116
|
+
if (!me.bot) throw new Error("Discord identity is not a bot");
|
|
117
|
+
const channel = await api.getChannel(channelId);
|
|
118
|
+
if (channel.id !== channelId || ![0, 1, 5, 10, 11, 12].includes(channel.type)) throw new Error("Discord destination must be a text channel, thread, or bot DM");
|
|
119
|
+
await saveConfigPatch(raw => {
|
|
120
|
+
const previous = raw.notifications?.discord;
|
|
121
|
+
const unchanged = previous?.botToken === token && previous.channelId === channelId;
|
|
122
|
+
const owners = allowedUserIds ?? (unchanged ? previous.allowedUserIds : undefined);
|
|
123
|
+
if (!owners?.length || !owners.every(id => /^[1-9]\d{0,19}$/.test(id))) throw new Error("Discord requires explicit allowed user IDs; repeat --allowed-user-ids after token or destination changes");
|
|
124
|
+
return { notifications: { ...raw.notifications, enabled: true, discord: { ...(unchanged ? previous : {}), botToken: token, channelId, allowedUserIds: owners } } };
|
|
125
|
+
});
|
|
126
|
+
console.log(`Discord bot verified; channelId=${channelId}. Notifications enabled.`);
|
|
127
|
+
console.log("Guild channels require MESSAGE CONTENT intent and View Channel/Send Messages permissions. Setup did not send a message.");
|
|
128
|
+
console.log("Token rotation or destination changes require an explicit user allowlist.");
|
|
129
|
+
} else {
|
|
130
|
+
const api = new TelegramApi(token, boundedFetch(opts));
|
|
131
|
+
const me = await api.getMe();
|
|
132
|
+
if (!me.ok || !me.result?.is_bot) throw new Error("Telegram getMe failed: invalid bot credentials or API unavailable");
|
|
133
|
+
const botId = String(me.result.id);
|
|
134
|
+
let chatId = flags.chatId;
|
|
135
|
+
let pairedUserId: string | undefined;
|
|
136
|
+
if (!chatId) {
|
|
137
|
+
if (!opts.prompt && !process.stdin.isTTY) throw new Error("Telegram auto-pairing requires a TTY; use explicit --chat-id otherwise");
|
|
138
|
+
const lock = await acquireDaemonLock("pairing");
|
|
139
|
+
if (!lock) throw new Error("A daemon or pairing process owns getUpdates; stop the daemon before auto-pairing, or use explicit --chat-id");
|
|
140
|
+
try {
|
|
141
|
+
const challenge = `/start jeo_${opts.pairingChallenge?.() ?? crypto.randomUUID()}`;
|
|
142
|
+
console.log(`Send exactly '${challenge}' to @${me.result.username} in a private chat within 120 seconds.`);
|
|
52
143
|
let offset: number | undefined;
|
|
53
|
-
const
|
|
54
|
-
|
|
144
|
+
const now = opts.now ?? Date.now;
|
|
145
|
+
const startedAt = now();
|
|
146
|
+
const deadline = startedAt + 120_000;
|
|
147
|
+
while (now() < deadline && !chatId) {
|
|
55
148
|
const updates = await api.getUpdates(offset, 5);
|
|
56
|
-
if (updates.ok)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
149
|
+
if (!updates.ok || !Array.isArray(updates.result)) throw new Error("Telegram pairing getUpdates failed (check bot polling ownership or credentials)");
|
|
150
|
+
for (const update of updates.result) {
|
|
151
|
+
if (!Number.isSafeInteger(update.update_id) || (offset !== undefined && update.update_id < offset)) continue;
|
|
152
|
+
offset = update.update_id + 1;
|
|
153
|
+
const msg = update.message;
|
|
154
|
+
if (msg?.chat.type === "private" && msg.date >= Math.floor(startedAt / 1000) && msg.text === challenge && msg.from && !msg.from.is_bot && String(msg.from.id) === String(msg.chat.id)) {
|
|
155
|
+
chatId = String(msg.chat.id);
|
|
156
|
+
pairedUserId = String(msg.from.id);
|
|
157
|
+
break;
|
|
64
158
|
}
|
|
65
159
|
}
|
|
66
160
|
}
|
|
67
|
-
if (!chatId)
|
|
68
|
-
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
console.log(`Paired private chat id ${chatId}.`);
|
|
72
|
-
}
|
|
73
|
-
} finally {
|
|
74
|
-
rl.close();
|
|
75
|
-
}
|
|
76
|
-
} else {
|
|
77
|
-
const api = new TelegramApi(token);
|
|
78
|
-
const me = await api.getMe();
|
|
79
|
-
if (!me.ok) {
|
|
80
|
-
console.log(`Telegram getMe failed: ${me.description ?? "invalid token"}.`);
|
|
81
|
-
return;
|
|
161
|
+
if (!chatId) throw new Error("Telegram pairing timed out; no matching private challenge received");
|
|
162
|
+
} finally { await lock.release(); }
|
|
82
163
|
}
|
|
164
|
+
const chat = await api.getChat(chatId);
|
|
165
|
+
if (!chat.ok || !chat.result?.type) throw new Error("Telegram chat lookup failed; check chat ID and bot access");
|
|
166
|
+
await saveConfigPatch(raw => {
|
|
167
|
+
const previous = raw.notifications?.telegram;
|
|
168
|
+
const sameDestination = previous?.chatId === chatId && (
|
|
169
|
+
previous.botToken === token || previous.botToken?.match(/^([1-9]\d*):/)?.[1] === botId
|
|
170
|
+
);
|
|
171
|
+
const owners = allowedUserIds ?? (sameDestination ? previous.allowedUserIds : undefined) ?? (pairedUserId ? [pairedUserId] : undefined);
|
|
172
|
+
if (chat.result!.type !== "private" && !owners?.length) throw new Error("Telegram groups require explicit --allowed-user-ids");
|
|
173
|
+
return { notifications: { ...raw.notifications, enabled: true, telegram: { ...(sameDestination ? previous : {}), botToken: token, chatId, ...(owners ? { allowedUserIds: owners } : {}) } } };
|
|
174
|
+
});
|
|
175
|
+
console.log(`Telegram bot verified; chatId=${chatId}. Notifications enabled. Setup did not send a message.`);
|
|
83
176
|
}
|
|
84
|
-
|
|
85
|
-
await saveConfigPatch(raw => ({
|
|
86
|
-
notifications: { ...raw.notifications, enabled: true, telegram: { botToken: token, chatId } },
|
|
87
|
-
}));
|
|
88
|
-
console.log(`\nNotifications enabled. botToken=${maskToken(token!)} chatId=${chatId}`);
|
|
89
|
-
console.log("Start the daemon with 'jeo daemon start', then run a detached subagent (task {detached:true}) to see it appear in Telegram.");
|
|
177
|
+
console.log("Run 'jeo daemon start' (or reload if already running) to apply. Use 'jeo notify health --provider " + flags.provider + "' for read-only API checks; 'jeo notify test --provider " + flags.provider + "' explicitly sends a test message.");
|
|
90
178
|
}
|
|
91
179
|
|
|
92
|
-
async function runStatus(): Promise<void> {
|
|
180
|
+
async function runStatus(provider: Provider): Promise<void> {
|
|
93
181
|
const config = await readGlobalConfig();
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
const chatId = config.notifications?.telegram?.chatId;
|
|
182
|
+
const n = config.notifications;
|
|
183
|
+
const channel = n?.[provider];
|
|
97
184
|
const status = await daemonStatus();
|
|
98
|
-
console.log(`enabled=${enabled}`);
|
|
99
|
-
console.log(`botToken=${botToken ?
|
|
100
|
-
console.log(`
|
|
101
|
-
console.log(`
|
|
185
|
+
console.log(`provider=${provider} enabled=${Boolean(n?.enabled)}`);
|
|
186
|
+
console.log(`botToken=${channel?.botToken ? "(set)" : "(not set)"}`);
|
|
187
|
+
if (provider === "slack") console.log(`appToken=${n?.slack?.appToken ? "(set)" : "(not set)"}`);
|
|
188
|
+
console.log(provider === "telegram" ? `chatId=${n?.telegram?.chatId ?? "(not set)"}` : `channelId=${n?.[provider]?.channelId ?? "(not set)"}`);
|
|
189
|
+
console.log(`allowedUserIds=${channel?.allowedUserIds?.join(",") ?? (provider === "telegram" ? "private chat owner only" : "(not set; inbound disabled)")}`);
|
|
190
|
+
console.log(`daemon=${status.pairing ? "pairing owns polling (daemon not running)" : status.running ? `process ${status.ready ? "initialized" : "initializing"} (pid ${status.pid}; transport connectivity not checked)` : status.stale ? "stale" : "stopped"}`);
|
|
191
|
+
console.log("Channel health not checked; use 'jeo notify health --provider " + provider + "'.");
|
|
102
192
|
}
|
|
103
193
|
|
|
104
|
-
|
|
194
|
+
async function runHealth(provider: Provider, send: boolean, opts: NotifyOptions): Promise<void> {
|
|
195
|
+
const n = (await readGlobalConfig()).notifications;
|
|
196
|
+
if (provider === "slack") {
|
|
197
|
+
const cfg = n?.slack;
|
|
198
|
+
if (!cfg?.botToken || !cfg.appToken || !cfg.channelId || !cfg.allowedUserIds?.length) throw new Error("Slack is incomplete: bot token, app token, channel ID and allowed user IDs are required");
|
|
199
|
+
const api = new SlackApi(cfg.botToken, cfg.appToken, { fetchImpl: boundedFetch(opts) });
|
|
200
|
+
try {
|
|
201
|
+
await verifySlack(api, cfg.channelId);
|
|
202
|
+
if (send) await api.sendMessage(cfg.channelId, "jeo notification test");
|
|
203
|
+
} finally { api.stop(); }
|
|
204
|
+
if (!send) console.log("Slack Socket Mode URL verified; app-token ownership, socket connectivity and send permissions are not tested until daemon handshake/test.");
|
|
205
|
+
} else if (provider === "discord") {
|
|
206
|
+
const cfg = n?.discord;
|
|
207
|
+
if (!cfg?.botToken || !cfg.channelId || !cfg.allowedUserIds?.length) throw new Error("Discord is incomplete: bot token, channel ID and allowed user IDs are required");
|
|
208
|
+
const api = new DiscordApi(cfg.botToken, { fetchImpl: boundedFetch(opts) });
|
|
209
|
+
const me = await api.getMe();
|
|
210
|
+
const channel = await api.getChannel(cfg.channelId);
|
|
211
|
+
if (!me.bot || channel.id !== cfg.channelId || ![0, 1, 5, 10, 11, 12].includes(channel.type)) throw new Error("Discord identity/channel validation failed");
|
|
212
|
+
if (send) await api.sendMessage(cfg.channelId, "jeo notification test");
|
|
213
|
+
} else {
|
|
214
|
+
const cfg = n?.telegram;
|
|
215
|
+
if (!cfg?.botToken || !cfg.chatId) throw new Error("Telegram is incomplete: bot token and chat ID are required");
|
|
216
|
+
const api = new TelegramApi(cfg.botToken, boundedFetch(opts));
|
|
217
|
+
const me = await api.getMe();
|
|
218
|
+
const chat = await api.getChat(cfg.chatId);
|
|
219
|
+
if (!me.ok || !me.result?.is_bot || !chat.ok || !chat.result?.type) throw new Error("Telegram bot/chat validation failed (check credentials and access)");
|
|
220
|
+
if (chat.result.type !== "private" && !cfg.allowedUserIds?.length) throw new Error("Telegram groups require an explicit user allowlist");
|
|
221
|
+
if (send && !(await api.sendMessage(cfg.chatId, "jeo notification test", { messageThreadId: cfg.topicId })).ok) throw new Error("Telegram test send failed (check permissions/rate limit)");
|
|
222
|
+
}
|
|
223
|
+
console.log(`${provider}: ${send ? "test message sent" : "bot identity and destination accessible (read-only; Gateway/poll connectivity and send permissions not tested)"}. enabled=${Boolean(n?.enabled)}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function runNotifyCommand(args: string[], opts: NotifyOptions = {}): Promise<void> {
|
|
105
227
|
const sub = args[0] ?? "status";
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
228
|
+
try {
|
|
229
|
+
const flags = parseFlags(args.slice(1));
|
|
230
|
+
if (sub === "setup") return await runSetup(flags, opts);
|
|
231
|
+
if (sub === "status") return await runStatus(flags.provider);
|
|
232
|
+
if (sub === "health" || sub === "test") return await runHealth(flags.provider, sub === "test", opts);
|
|
233
|
+
throw new Error("Usage: jeo notify [setup|status|health|test] [--provider telegram|discord|slack]");
|
|
234
|
+
} catch (error) {
|
|
235
|
+
// Only emit our known configuration/API errors, never fetch URLs or token-bearing bodies.
|
|
236
|
+
const message = error instanceof Error ? error.message : "request failed";
|
|
237
|
+
const safe = /^(Missing |Unknown notify|Provider |Allowed user|Telegram |Discord |Slack |Bot token|Missing setup|A daemon|Notification lock recovery|Usage:)/.test(message) && !message.includes("https:") && !message.includes("http:");
|
|
238
|
+
console.error(`Notification ${sub} failed: ${safe ? message : "API or storage request failed; check credentials, connectivity and permissions"}${(error instanceof DiscordApiError || error instanceof SlackApiError) && error.status ? ` (HTTP ${error.status})` : ""}`);
|
|
239
|
+
process.exitCode = 1;
|
|
240
|
+
}
|
|
109
241
|
}
|
package/src/commands/ralplan.ts
CHANGED
|
@@ -403,5 +403,6 @@ export async function runRalplanEngine(opts: RalplanEngineOptions = {}): Promise
|
|
|
403
403
|
}
|
|
404
404
|
|
|
405
405
|
export async function runRalplanCommand(): Promise<void> {
|
|
406
|
-
await runRalplanEngine();
|
|
406
|
+
const result = await runRalplanEngine();
|
|
407
|
+
if (!result.ok) process.exitCode = 1;
|
|
407
408
|
}
|
package/src/commands/team.ts
CHANGED
|
@@ -220,16 +220,18 @@ export async function runTeamEngine(opts: TeamEngineOptions = {}): Promise<{ ok:
|
|
|
220
220
|
return { ok: false, reason: err.message };
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
//
|
|
224
|
-
if (planState.consensus_hash) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
223
|
+
// Execution requires the exact reviewed bytes, including for legacy state.
|
|
224
|
+
if (!planState.consensus_hash) {
|
|
225
|
+
log(`[ERROR] Plan lacks a consensus review hash. Re-run 'jeo ralplan', then approve and execute again.`);
|
|
226
|
+
return { ok: false, reason: "Plan lacks a consensus review hash" };
|
|
227
|
+
}
|
|
228
|
+
const currentHash = createHash("sha256").update(planContent).digest("hex");
|
|
229
|
+
if (currentHash !== planState.consensus_hash) {
|
|
230
|
+
log(
|
|
231
|
+
`[ERROR] Plan file has been modified since it was reviewed by the consensus critic.\n` +
|
|
232
|
+
` Re-run 'jeo ralplan' to let the critic review the updated plan, then approve and execute again.`
|
|
233
|
+
);
|
|
234
|
+
return { ok: false, reason: "Plan file modified since consensus review" };
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
let rawPlan: any;
|
|
@@ -288,6 +290,7 @@ export async function runTeamEngine(opts: TeamEngineOptions = {}): Promise<{ ok:
|
|
|
288
290
|
skill: "team" as const,
|
|
289
291
|
slug: planState.slug,
|
|
290
292
|
plan_path: planPath,
|
|
293
|
+
consensus_hash: planState.consensus_hash,
|
|
291
294
|
completed_tasks: [],
|
|
292
295
|
pending_tasks: [...tasks],
|
|
293
296
|
};
|
|
@@ -302,8 +305,9 @@ export async function runTeamEngine(opts: TeamEngineOptions = {}): Promise<{ ok:
|
|
|
302
305
|
// Round-7 #1 (architect ref 7-Round7Workflow): a team-state left over from a
|
|
303
306
|
// PREVIOUS plan must never be reused — pending=[] from plan A would make plan B
|
|
304
307
|
// no-op into a false "all executed" success, and a mid-flight leftover would run
|
|
305
|
-
// plan-A task text under plan-B roles.
|
|
306
|
-
|
|
308
|
+
// plan-A task text under plan-B roles. Changed bytes or legacy unbound state
|
|
309
|
+
// also reinitialize execution, even when the plan path and slug are unchanged.
|
|
310
|
+
if (teamState.plan_path !== planPath || teamState.slug !== planState.slug || teamState.consensus_hash !== planState.consensus_hash) {
|
|
307
311
|
log(`${categoryBadge("progress")} New plan detected (${planPath}) — restarting execution from its task list.`);
|
|
308
312
|
teamState = {
|
|
309
313
|
active: true,
|
|
@@ -311,6 +315,7 @@ export async function runTeamEngine(opts: TeamEngineOptions = {}): Promise<{ ok:
|
|
|
311
315
|
skill: "team" as const,
|
|
312
316
|
slug: planState.slug,
|
|
313
317
|
plan_path: planPath,
|
|
318
|
+
consensus_hash: planState.consensus_hash,
|
|
314
319
|
completed_tasks: [],
|
|
315
320
|
pending_tasks: [...tasks],
|
|
316
321
|
};
|
|
@@ -209,5 +209,6 @@ export async function runUltragoalEngine(opts: UltragoalEngineOptions = {}): Pro
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
export async function runUltragoalCommand(): Promise<void> {
|
|
212
|
-
await runUltragoalEngine();
|
|
212
|
+
const result = await runUltragoalEngine();
|
|
213
|
+
if (!result.ok) process.exitCode = 1;
|
|
213
214
|
}
|