@gajae-code/coding-agent 0.12.7 → 0.12.10
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 +48 -0
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/commands/notify.d.ts +4 -0
- package/dist/types/commands/sdk.d.ts +23 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/config/model-discovery-manager.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/resolve-config-value.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +17 -1
- package/dist/types/discovery/agents-md.d.ts +11 -1
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +20 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/broker/broker.d.ts +5 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
- package/dist/types/sdk/bus/config.d.ts +75 -113
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
- package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
- package/dist/types/sdk/bus/notification-service.d.ts +54 -4
- package/dist/types/sdk/bus/session-control.d.ts +8 -3
- package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
- package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/client/discovery.d.ts +2 -1
- package/dist/types/sdk/host/query/handlers.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/providers.d.ts +22 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +19 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +3 -2
- package/dist/types/session/internal/session-open-errors.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/session/session-storage.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +12 -1
- package/dist/types/tools/composer-bash-policy.d.ts +9 -1
- package/package.json +10 -8
- package/scripts/build-sdk-package-smoke.ts +51 -0
- package/scripts/capture-notifications-settings-showcase.ts +1 -1
- package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
- package/src/cli/notify-cli.ts +215 -99
- package/src/commands/notify.ts +19 -8
- package/src/commands/sdk.ts +54 -0
- package/src/config/file-lock.ts +6 -0
- package/src/config/model-discovery-manager.ts +50 -8
- package/src/config/model-profiles.ts +8 -0
- package/src/config/model-registry.ts +866 -76
- package/src/config/resolve-config-value.ts +10 -8
- package/src/config/settings-schema.ts +25 -15
- package/src/config/settings.ts +15 -15
- package/src/discovery/agents-md.ts +76 -19
- package/src/gjc-runtime/psmux-detect.ts +12 -16
- package/src/gjc-runtime/session-state-sidecar.ts +12 -2
- package/src/gjc-runtime/tmux-sessions.ts +8 -1
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/main.ts +43 -7
- package/src/modes/DESIGN.md +11 -11
- package/src/modes/components/extensions/inspector-panel.ts +9 -5
- package/src/modes/components/hook-selector.ts +155 -44
- package/src/modes/components/model-selector.ts +242 -49
- package/src/modes/components/notifications-settings-editor.ts +631 -85
- package/src/modes/controllers/extension-ui-controller.ts +114 -15
- package/src/modes/controllers/selector-controller.ts +525 -93
- package/src/modes/print-mode.ts +3 -0
- package/src/modes/types.ts +6 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/sdk/broker/broker.ts +54 -10
- package/src/sdk/broker/lifecycle-ledger.ts +34 -1
- package/src/sdk/broker/lifecycle.ts +47 -50
- package/src/sdk/bus/chat-daemon-cli.ts +12 -5
- package/src/sdk/bus/chat-daemon-control.ts +35 -6
- package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
- package/src/sdk/bus/config.ts +534 -286
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/discord-live-provider.ts +57 -2
- package/src/sdk/bus/discord-provider.ts +21 -0
- package/src/sdk/bus/index.ts +97 -35
- package/src/sdk/bus/notification-orchestration.ts +371 -68
- package/src/sdk/bus/notification-service.ts +571 -137
- package/src/sdk/bus/session-control.ts +119 -44
- package/src/sdk/bus/slack-live-provider.ts +78 -2
- package/src/sdk/bus/slack-provider.ts +24 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/bus/telegram-daemon-control.ts +3 -3
- package/src/sdk/bus/telegram-daemon.ts +23 -15
- package/src/sdk/client/discovery.ts +10 -4
- package/src/sdk/host/query/handlers.ts +20 -1
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +5 -2
- package/src/sdk/providers.ts +51 -0
- package/src/sdk/session-directory.ts +1 -0
- package/src/sdk/session.ts +34 -14
- package/src/session/agent-session.ts +109 -37
- package/src/session/internal/managed-session-scope.ts +981 -450
- package/src/session/internal/managed-session-storage.ts +418 -47
- package/src/session/internal/native-publish-outcome.ts +15 -3
- package/src/session/internal/session-open-errors.ts +11 -0
- package/src/session/session-manager.ts +27 -3
- package/src/session/session-storage.ts +72 -5
- package/src/slash-commands/builtin-registry.ts +117 -15
- package/src/system-prompt.ts +28 -15
- package/src/task/index.ts +9 -3
- package/src/tools/ask.ts +1 -1
- package/src/tools/bash.ts +4 -2
- package/src/tools/composer-bash-policy.ts +21 -5
- package/src/tools/telegram-send.ts +4 -2
- package/src/utils/command-args.ts +19 -30
- package/src/utils/lang-from-path.ts +2 -4
package/src/sdk/bus/config.ts
CHANGED
|
@@ -49,39 +49,54 @@ export function readTelegramActivationMarkers(value?: unknown): TelegramActivati
|
|
|
49
49
|
return markers;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export type NotificationProvider = "telegram" | "discord" | "slack";
|
|
53
|
+
export type NotificationRuntime = "inactive" | "starting" | "ready" | "attached" | "blocked" | "failed";
|
|
54
|
+
export type ProviderSecretDisposition = "keep" | "replace" | "remove";
|
|
55
|
+
export type ProviderResolutionIssueCode = "missing" | "blank" | "wrong_type" | "invalid_container" | "contradictory";
|
|
56
|
+
|
|
57
|
+
export interface ProviderResolutionIssue {
|
|
58
|
+
path: string;
|
|
59
|
+
code: ProviderResolutionIssueCode;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type NotificationProviderIssueCode = ProviderResolutionIssueCode;
|
|
63
|
+
export type NotificationProviderIssue = ProviderResolutionIssue;
|
|
64
|
+
|
|
65
|
+
export interface ProviderResolution {
|
|
66
|
+
provider: NotificationProvider;
|
|
67
|
+
configured: boolean;
|
|
68
|
+
quarantined: boolean;
|
|
69
|
+
desiredEnabled: boolean;
|
|
70
|
+
desiredSource: "explicit" | "legacy";
|
|
71
|
+
effectiveEnabled: boolean;
|
|
72
|
+
issues: readonly ProviderResolutionIssue[];
|
|
73
|
+
}
|
|
74
|
+
const NOTIFICATION_PROVIDERS = ["telegram", "discord", "slack"] as const;
|
|
75
|
+
|
|
52
76
|
export interface NotificationSettingsSnapshot {
|
|
53
77
|
enabled: boolean;
|
|
54
78
|
telegram: {
|
|
79
|
+
enabled?: boolean;
|
|
55
80
|
botToken?: string;
|
|
56
81
|
chatId?: string;
|
|
57
82
|
activation?: Record<string, unknown>;
|
|
58
83
|
sound: "all" | "important" | "none";
|
|
59
|
-
btw: {
|
|
60
|
-
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
};
|
|
65
|
-
richDraft: {
|
|
66
|
-
enabled: boolean;
|
|
67
|
-
};
|
|
68
|
-
toolActivity: {
|
|
69
|
-
enabled: boolean;
|
|
70
|
-
};
|
|
71
|
-
streaming: {
|
|
72
|
-
enabled: boolean;
|
|
73
|
-
};
|
|
74
|
-
topics: {
|
|
75
|
-
nameTemplate?: string;
|
|
76
|
-
};
|
|
84
|
+
btw: { enabled: boolean };
|
|
85
|
+
rich: { enabled: boolean };
|
|
86
|
+
richDraft: { enabled: boolean };
|
|
87
|
+
toolActivity: { enabled: boolean };
|
|
88
|
+
streaming: { enabled: boolean };
|
|
89
|
+
topics: { nameTemplate?: string };
|
|
77
90
|
};
|
|
78
91
|
discord: {
|
|
92
|
+
enabled?: boolean;
|
|
79
93
|
botToken?: string;
|
|
80
94
|
applicationId?: string;
|
|
81
95
|
guildId?: string;
|
|
82
96
|
parentChannelId?: string;
|
|
83
97
|
};
|
|
84
98
|
slack: {
|
|
99
|
+
enabled?: boolean;
|
|
85
100
|
botToken?: string;
|
|
86
101
|
appToken?: string;
|
|
87
102
|
workspaceId?: string;
|
|
@@ -92,120 +107,301 @@ export interface NotificationSettingsSnapshot {
|
|
|
92
107
|
verbosity: "lean" | "verbose";
|
|
93
108
|
sessionScope: "all" | "primary";
|
|
94
109
|
idleTimeoutMs: number;
|
|
110
|
+
/** Safe provider-local validation issues. Raw values never cross this boundary. */
|
|
111
|
+
providerIssues?: Partial<Record<NotificationProvider, readonly ProviderResolutionIssue[]>>;
|
|
95
112
|
}
|
|
96
113
|
|
|
97
114
|
/**
|
|
98
115
|
* Narrow settings boundary for remote notification identity and behavior.
|
|
99
|
-
* Implementations
|
|
100
|
-
*
|
|
116
|
+
* Implementations return only user-global values; project settings and runtime
|
|
117
|
+
* overrides are intentionally excluded.
|
|
101
118
|
*/
|
|
102
119
|
export interface NotificationSettingsReader {
|
|
103
120
|
getNotificationSettingsSnapshot(): NotificationSettingsSnapshot;
|
|
104
121
|
getAgentDir(): string;
|
|
105
122
|
}
|
|
123
|
+
|
|
106
124
|
function notificationConfigurationError(): Error {
|
|
107
125
|
return new Error("gjc_notify_daemon_invalid_configuration");
|
|
108
126
|
}
|
|
109
127
|
|
|
110
|
-
|
|
128
|
+
type NotificationObject = Record<string, unknown>;
|
|
129
|
+
|
|
130
|
+
function notificationObject(value: unknown): NotificationObject {
|
|
111
131
|
if (value === undefined) return {};
|
|
112
132
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw notificationConfigurationError();
|
|
113
|
-
return value as
|
|
133
|
+
return value as NotificationObject;
|
|
114
134
|
}
|
|
115
135
|
|
|
116
|
-
function
|
|
117
|
-
if (value === undefined) return
|
|
118
|
-
if (typeof value
|
|
119
|
-
|
|
136
|
+
function providerObject(value: unknown, pathName: string, issues: ProviderResolutionIssue[]): NotificationObject {
|
|
137
|
+
if (value === undefined) return {};
|
|
138
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
139
|
+
issues.push({ path: pathName, code: "invalid_container" });
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
return value as NotificationObject;
|
|
120
143
|
}
|
|
121
144
|
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
145
|
+
function addProviderIssue(
|
|
146
|
+
issues: ProviderResolutionIssue[],
|
|
147
|
+
pathName: string,
|
|
148
|
+
code: ProviderResolutionIssueCode,
|
|
149
|
+
): void {
|
|
150
|
+
if (!issues.some(issue => issue.path === pathName && issue.code === code)) issues.push({ path: pathName, code });
|
|
126
151
|
}
|
|
127
152
|
|
|
128
|
-
function
|
|
153
|
+
function providerString(
|
|
154
|
+
container: NotificationObject,
|
|
155
|
+
key: string,
|
|
156
|
+
pathName: string,
|
|
157
|
+
issues: ProviderResolutionIssue[],
|
|
158
|
+
): string | undefined {
|
|
159
|
+
const value = container[key];
|
|
160
|
+
if (value === undefined) return undefined;
|
|
161
|
+
if (typeof value !== "string") {
|
|
162
|
+
addProviderIssue(issues, pathName, "wrong_type");
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
if (value.trim().length === 0) {
|
|
166
|
+
addProviderIssue(issues, pathName, "blank");
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function providerBoolean(
|
|
173
|
+
container: NotificationObject,
|
|
174
|
+
key: string,
|
|
175
|
+
pathName: string,
|
|
176
|
+
fallback: boolean,
|
|
177
|
+
issues: ProviderResolutionIssue[],
|
|
178
|
+
): boolean {
|
|
179
|
+
const value = container[key];
|
|
180
|
+
if (value === undefined) return fallback;
|
|
181
|
+
if (typeof value !== "boolean") {
|
|
182
|
+
addProviderIssue(issues, pathName, "wrong_type");
|
|
183
|
+
return fallback;
|
|
184
|
+
}
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function providerChoice<T extends string>(
|
|
189
|
+
container: NotificationObject,
|
|
190
|
+
key: string,
|
|
191
|
+
pathName: string,
|
|
192
|
+
fallback: T,
|
|
193
|
+
choices: readonly T[],
|
|
194
|
+
issues: ProviderResolutionIssue[],
|
|
195
|
+
): T {
|
|
196
|
+
const value = container[key];
|
|
129
197
|
if (value === undefined) return fallback;
|
|
130
198
|
if (typeof value === "string" && choices.includes(value as T)) return value as T;
|
|
131
|
-
|
|
199
|
+
addProviderIssue(issues, pathName, "wrong_type");
|
|
200
|
+
return fallback;
|
|
132
201
|
}
|
|
133
202
|
|
|
134
|
-
function
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
203
|
+
function providerActivation(
|
|
204
|
+
container: NotificationObject,
|
|
205
|
+
pathName: string,
|
|
206
|
+
issues: ProviderResolutionIssue[],
|
|
207
|
+
): Record<string, unknown> | undefined {
|
|
208
|
+
const value = container.activation;
|
|
209
|
+
if (value === undefined) return undefined;
|
|
210
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
211
|
+
addProviderIssue(issues, pathName, "invalid_container");
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
const safe = readTelegramActivationMarkers(value);
|
|
215
|
+
if (Object.keys(safe).length !== Object.keys(value as NotificationObject).length) {
|
|
216
|
+
addProviderIssue(issues, pathName, "contradictory");
|
|
217
|
+
}
|
|
218
|
+
return Object.keys(safe).length === 0 ? undefined : safe;
|
|
138
219
|
}
|
|
139
220
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
221
|
+
function providerTopics(
|
|
222
|
+
container: NotificationObject,
|
|
223
|
+
pathName: string,
|
|
224
|
+
issues: ProviderResolutionIssue[],
|
|
225
|
+
): { nameTemplate?: string } {
|
|
226
|
+
const topics = providerObject(container.topics, pathName, issues);
|
|
227
|
+
return { nameTemplate: providerString(topics, "nameTemplate", `${pathName}.nameTemplate`, issues) };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function providerEnabled(
|
|
231
|
+
container: NotificationObject,
|
|
232
|
+
pathName: string,
|
|
233
|
+
issues: ProviderResolutionIssue[],
|
|
234
|
+
): boolean | undefined {
|
|
235
|
+
const value = container.enabled;
|
|
236
|
+
if (value === undefined) return undefined;
|
|
237
|
+
if (typeof value !== "boolean") {
|
|
238
|
+
addProviderIssue(issues, pathName, "wrong_type");
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function addMissingRequiredProviderIssues(
|
|
245
|
+
container: NotificationObject,
|
|
246
|
+
required: readonly string[],
|
|
247
|
+
basePath: string,
|
|
248
|
+
issues: ProviderResolutionIssue[],
|
|
249
|
+
explicitlyEnabled: boolean,
|
|
250
|
+
): void {
|
|
251
|
+
const anyRequiredPresent = required.some(key => container[key] !== undefined);
|
|
252
|
+
if (!anyRequiredPresent && !explicitlyEnabled) return;
|
|
253
|
+
for (const key of required) {
|
|
254
|
+
if (container[key] === undefined) addProviderIssue(issues, `${basePath}.${key}`, "missing");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function providerIssuesSnapshot(
|
|
259
|
+
provider: NotificationProvider,
|
|
260
|
+
issues: readonly ProviderResolutionIssue[],
|
|
261
|
+
): Partial<Record<NotificationProvider, readonly ProviderResolutionIssue[]>> | undefined {
|
|
262
|
+
return issues.length === 0 ? undefined : { [provider]: issues.map(issue => ({ ...issue })) };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Validate and snapshot raw global notification configuration without leaking invalid provider values. */
|
|
144
266
|
export function parseNotificationSettingsSnapshot(rawConfig?: unknown): NotificationSettingsSnapshot {
|
|
145
|
-
const root =
|
|
146
|
-
const notifications =
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
267
|
+
const root = notificationObject(rawConfig);
|
|
268
|
+
const notifications = notificationObject(root.notifications);
|
|
269
|
+
const telegramIssues: ProviderResolutionIssue[] = [];
|
|
270
|
+
const discordIssues: ProviderResolutionIssue[] = [];
|
|
271
|
+
const slackIssues: ProviderResolutionIssue[] = [];
|
|
272
|
+
const telegram = providerObject(notifications.telegram, "notifications.telegram", telegramIssues);
|
|
273
|
+
const discord = providerObject(notifications.discord, "notifications.discord", discordIssues);
|
|
274
|
+
const slack = providerObject(notifications.slack, "notifications.slack", slackIssues);
|
|
275
|
+
const btw = providerObject(telegram.btw, "notifications.telegram.btw", telegramIssues);
|
|
276
|
+
const rich = providerObject(telegram.rich, "notifications.telegram.rich", telegramIssues);
|
|
277
|
+
const richDraft = providerObject(telegram.richDraft, "notifications.telegram.richDraft", telegramIssues);
|
|
278
|
+
const toolActivity = providerObject(telegram.toolActivity, "notifications.telegram.toolActivity", telegramIssues);
|
|
279
|
+
const streaming = providerObject(telegram.streaming, "notifications.telegram.streaming", telegramIssues);
|
|
280
|
+
const activation = providerActivation(telegram, "notifications.telegram.activation", telegramIssues);
|
|
281
|
+
const daemon = notificationObject(notifications.daemon);
|
|
282
|
+
const telegramEnabled = providerEnabled(telegram, "notifications.telegram.enabled", telegramIssues);
|
|
283
|
+
const discordEnabled = providerEnabled(discord, "notifications.discord.enabled", discordIssues);
|
|
284
|
+
const slackEnabled = providerEnabled(slack, "notifications.slack.enabled", slackIssues);
|
|
285
|
+
addMissingRequiredProviderIssues(
|
|
286
|
+
telegram,
|
|
287
|
+
["botToken", "chatId"],
|
|
288
|
+
"notifications.telegram",
|
|
289
|
+
telegramIssues,
|
|
290
|
+
telegramEnabled === true,
|
|
291
|
+
);
|
|
292
|
+
addMissingRequiredProviderIssues(
|
|
293
|
+
discord,
|
|
294
|
+
["botToken", "applicationId", "guildId", "parentChannelId"],
|
|
295
|
+
"notifications.discord",
|
|
296
|
+
discordIssues,
|
|
297
|
+
discordEnabled === true,
|
|
298
|
+
);
|
|
299
|
+
addMissingRequiredProviderIssues(
|
|
300
|
+
slack,
|
|
301
|
+
["botToken", "appToken", "workspaceId", "channelId"],
|
|
302
|
+
"notifications.slack",
|
|
303
|
+
slackIssues,
|
|
304
|
+
slackEnabled === true,
|
|
305
|
+
);
|
|
306
|
+
const telegramSnapshot: NotificationSettingsSnapshot["telegram"] = {
|
|
307
|
+
...(telegramEnabled === undefined ? {} : { enabled: telegramEnabled }),
|
|
308
|
+
botToken: providerString(telegram, "botToken", "notifications.telegram.botToken", telegramIssues),
|
|
309
|
+
chatId: providerString(telegram, "chatId", "notifications.telegram.chatId", telegramIssues),
|
|
310
|
+
...(activation === undefined ? {} : { activation }),
|
|
311
|
+
sound: providerChoice(
|
|
312
|
+
telegram,
|
|
313
|
+
"sound",
|
|
314
|
+
"notifications.telegram.sound",
|
|
315
|
+
"all",
|
|
316
|
+
["all", "important", "none"],
|
|
317
|
+
telegramIssues,
|
|
318
|
+
),
|
|
319
|
+
btw: { enabled: providerBoolean(btw, "enabled", "notifications.telegram.btw.enabled", true, telegramIssues) },
|
|
320
|
+
rich: { enabled: providerBoolean(rich, "enabled", "notifications.telegram.rich.enabled", true, telegramIssues) },
|
|
321
|
+
richDraft: {
|
|
322
|
+
enabled: providerBoolean(
|
|
323
|
+
richDraft,
|
|
324
|
+
"enabled",
|
|
325
|
+
"notifications.telegram.richDraft.enabled",
|
|
326
|
+
false,
|
|
327
|
+
telegramIssues,
|
|
328
|
+
),
|
|
187
329
|
},
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
330
|
+
toolActivity: {
|
|
331
|
+
enabled: providerBoolean(
|
|
332
|
+
toolActivity,
|
|
333
|
+
"enabled",
|
|
334
|
+
"notifications.telegram.toolActivity.enabled",
|
|
335
|
+
false,
|
|
336
|
+
telegramIssues,
|
|
337
|
+
),
|
|
193
338
|
},
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
339
|
+
streaming: {
|
|
340
|
+
enabled: providerBoolean(
|
|
341
|
+
streaming,
|
|
342
|
+
"enabled",
|
|
343
|
+
"notifications.telegram.streaming.enabled",
|
|
344
|
+
true,
|
|
345
|
+
telegramIssues,
|
|
346
|
+
),
|
|
200
347
|
},
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
348
|
+
topics: providerTopics(telegram, "notifications.telegram.topics", telegramIssues),
|
|
349
|
+
};
|
|
350
|
+
const discordSnapshot: NotificationSettingsSnapshot["discord"] = {
|
|
351
|
+
...(discordEnabled === undefined ? {} : { enabled: discordEnabled }),
|
|
352
|
+
botToken: providerString(discord, "botToken", "notifications.discord.botToken", discordIssues),
|
|
353
|
+
applicationId: providerString(discord, "applicationId", "notifications.discord.applicationId", discordIssues),
|
|
354
|
+
guildId: providerString(discord, "guildId", "notifications.discord.guildId", discordIssues),
|
|
355
|
+
parentChannelId: providerString(
|
|
356
|
+
discord,
|
|
357
|
+
"parentChannelId",
|
|
358
|
+
"notifications.discord.parentChannelId",
|
|
359
|
+
discordIssues,
|
|
360
|
+
),
|
|
361
|
+
};
|
|
362
|
+
const slackSnapshot: NotificationSettingsSnapshot["slack"] = {
|
|
363
|
+
...(slackEnabled === undefined ? {} : { enabled: slackEnabled }),
|
|
364
|
+
botToken: providerString(slack, "botToken", "notifications.slack.botToken", slackIssues),
|
|
365
|
+
appToken: providerString(slack, "appToken", "notifications.slack.appToken", slackIssues),
|
|
366
|
+
workspaceId: providerString(slack, "workspaceId", "notifications.slack.workspaceId", slackIssues),
|
|
367
|
+
channelId: providerString(slack, "channelId", "notifications.slack.channelId", slackIssues),
|
|
368
|
+
authorizedUserId: providerString(slack, "authorizedUserId", "notifications.slack.authorizedUserId", slackIssues),
|
|
369
|
+
};
|
|
370
|
+
const providerIssues: Partial<Record<NotificationProvider, readonly ProviderResolutionIssue[]>> = {
|
|
371
|
+
...(providerIssuesSnapshot("telegram", telegramIssues) ?? {}),
|
|
372
|
+
...(providerIssuesSnapshot("discord", discordIssues) ?? {}),
|
|
373
|
+
...(providerIssuesSnapshot("slack", slackIssues) ?? {}),
|
|
374
|
+
};
|
|
375
|
+
const snapshot: NotificationSettingsSnapshot = {
|
|
376
|
+
enabled: notificationGlobalBoolean(notifications.enabled, false),
|
|
377
|
+
telegram: telegramSnapshot,
|
|
378
|
+
discord: discordSnapshot,
|
|
379
|
+
slack: slackSnapshot,
|
|
380
|
+
redact: notificationGlobalBoolean(notifications.redact, false),
|
|
381
|
+
verbosity: notificationGlobalChoice(notifications.verbosity, "lean", ["lean", "verbose"]),
|
|
382
|
+
sessionScope: notificationGlobalChoice(notifications.sessionScope, "all", ["all", "primary"]),
|
|
207
383
|
idleTimeoutMs: notificationIdleTimeoutMs(daemon.idleTimeoutMs),
|
|
208
384
|
};
|
|
385
|
+
if (Object.keys(providerIssues).length > 0) snapshot.providerIssues = providerIssues;
|
|
386
|
+
return snapshot;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function notificationGlobalBoolean(value: unknown, fallback: boolean): boolean {
|
|
390
|
+
if (value === undefined) return fallback;
|
|
391
|
+
if (typeof value !== "boolean") throw notificationConfigurationError();
|
|
392
|
+
return value;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function notificationGlobalChoice<T extends string>(value: unknown, fallback: T, choices: readonly T[]): T {
|
|
396
|
+
if (value === undefined) return fallback;
|
|
397
|
+
if (typeof value === "string" && choices.includes(value as T)) return value as T;
|
|
398
|
+
throw notificationConfigurationError();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function notificationIdleTimeoutMs(value: unknown): number {
|
|
402
|
+
if (value === undefined) return 60_000;
|
|
403
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
404
|
+
throw notificationConfigurationError();
|
|
209
405
|
}
|
|
210
406
|
|
|
211
407
|
export interface NotificationConfig {
|
|
@@ -213,13 +409,17 @@ export interface NotificationConfig {
|
|
|
213
409
|
botToken?: string;
|
|
214
410
|
activation?: TelegramActivationMarkers;
|
|
215
411
|
chatId?: string;
|
|
412
|
+
/** Optional nested Telegram intent metadata; absent means legacy configuration. */
|
|
413
|
+
telegram?: { enabled?: boolean };
|
|
216
414
|
discord: {
|
|
415
|
+
enabled?: boolean;
|
|
217
416
|
botToken?: string;
|
|
218
417
|
applicationId?: string;
|
|
219
418
|
guildId?: string;
|
|
220
419
|
parentChannelId?: string;
|
|
221
420
|
};
|
|
222
421
|
slack: {
|
|
422
|
+
enabled?: boolean;
|
|
223
423
|
botToken?: string;
|
|
224
424
|
appToken?: string;
|
|
225
425
|
workspaceId?: string;
|
|
@@ -228,51 +428,41 @@ export interface NotificationConfig {
|
|
|
228
428
|
};
|
|
229
429
|
redact: boolean;
|
|
230
430
|
verbosity: "lean" | "verbose";
|
|
231
|
-
/**
|
|
232
|
-
* Which sessions may register a notification endpoint. `all` (default)
|
|
233
|
-
* preserves historical behavior; `primary` suppresses GJC-spawned children
|
|
234
|
-
* (those carrying {@link SPAWN_PROVENANCE_ENV}) unless they explicitly opt in.
|
|
235
|
-
*/
|
|
236
431
|
sessionScope: "all" | "primary";
|
|
237
432
|
sound: "all" | "important" | "none";
|
|
238
433
|
idleTimeoutMs: number;
|
|
239
|
-
btw: {
|
|
240
|
-
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
enabled: boolean;
|
|
247
|
-
};
|
|
248
|
-
toolActivity: {
|
|
249
|
-
enabled: boolean;
|
|
250
|
-
};
|
|
251
|
-
streaming: {
|
|
252
|
-
enabled: boolean;
|
|
253
|
-
};
|
|
254
|
-
topics: {
|
|
255
|
-
/**
|
|
256
|
-
* Optional Telegram forum-topic name template with `{repo}`, `{branch}`,
|
|
257
|
-
* and `{title}` placeholders. Unset preserves the built-in
|
|
258
|
-
* `{repo}/{branch} - {title}` composition (with its title/repo/branch
|
|
259
|
-
* fallbacks).
|
|
260
|
-
*/
|
|
261
|
-
nameTemplate?: string;
|
|
262
|
-
};
|
|
434
|
+
btw: { enabled: boolean };
|
|
435
|
+
rich: { enabled: boolean };
|
|
436
|
+
richDraft: { enabled: boolean };
|
|
437
|
+
toolActivity: { enabled: boolean };
|
|
438
|
+
streaming: { enabled: boolean };
|
|
439
|
+
topics: { nameTemplate?: string };
|
|
440
|
+
providerIssues?: Partial<Record<NotificationProvider, readonly ProviderResolutionIssue[]>>;
|
|
263
441
|
}
|
|
264
442
|
|
|
265
|
-
|
|
266
|
-
export function getNotificationConfig(settings: NotificationSettingsReader): NotificationConfig {
|
|
267
|
-
const snapshot = settings.getNotificationSettingsSnapshot();
|
|
443
|
+
function notificationConfigFromSnapshot(snapshot: NotificationSettingsSnapshot): NotificationConfig {
|
|
268
444
|
const activation = readTelegramActivationMarkers(snapshot.telegram.activation);
|
|
269
|
-
|
|
445
|
+
const config: NotificationConfig = {
|
|
270
446
|
enabled: snapshot.enabled,
|
|
271
447
|
botToken: snapshot.telegram.botToken,
|
|
272
448
|
...(Object.keys(activation).length === 0 ? {} : { activation }),
|
|
273
449
|
chatId: snapshot.telegram.chatId,
|
|
274
|
-
|
|
275
|
-
|
|
450
|
+
...(snapshot.telegram.enabled === undefined ? {} : { telegram: { enabled: snapshot.telegram.enabled } }),
|
|
451
|
+
discord: {
|
|
452
|
+
...(snapshot.discord.enabled === undefined ? {} : { enabled: snapshot.discord.enabled }),
|
|
453
|
+
botToken: snapshot.discord.botToken,
|
|
454
|
+
applicationId: snapshot.discord.applicationId,
|
|
455
|
+
guildId: snapshot.discord.guildId,
|
|
456
|
+
parentChannelId: snapshot.discord.parentChannelId,
|
|
457
|
+
},
|
|
458
|
+
slack: {
|
|
459
|
+
...(snapshot.slack.enabled === undefined ? {} : { enabled: snapshot.slack.enabled }),
|
|
460
|
+
botToken: snapshot.slack.botToken,
|
|
461
|
+
appToken: snapshot.slack.appToken,
|
|
462
|
+
workspaceId: snapshot.slack.workspaceId,
|
|
463
|
+
channelId: snapshot.slack.channelId,
|
|
464
|
+
authorizedUserId: snapshot.slack.authorizedUserId,
|
|
465
|
+
},
|
|
276
466
|
redact: snapshot.redact,
|
|
277
467
|
verbosity: snapshot.verbosity,
|
|
278
468
|
sessionScope: snapshot.sessionScope,
|
|
@@ -285,40 +475,16 @@ export function getNotificationConfig(settings: NotificationSettingsReader): Not
|
|
|
285
475
|
streaming: snapshot.telegram.streaming,
|
|
286
476
|
topics: snapshot.telegram.topics,
|
|
287
477
|
};
|
|
478
|
+
if (snapshot.providerIssues) config.providerIssues = snapshot.providerIssues;
|
|
479
|
+
return config;
|
|
288
480
|
}
|
|
289
481
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
enabled: z.boolean().optional(),
|
|
295
|
-
discord: z
|
|
296
|
-
.object({
|
|
297
|
-
botToken: z.string().optional(),
|
|
298
|
-
applicationId: z.string().optional(),
|
|
299
|
-
guildId: z.string().optional(),
|
|
300
|
-
parentChannelId: z.string().optional(),
|
|
301
|
-
})
|
|
302
|
-
.passthrough()
|
|
303
|
-
.optional(),
|
|
304
|
-
slack: z
|
|
305
|
-
.object({
|
|
306
|
-
botToken: z.string().optional(),
|
|
307
|
-
appToken: z.string().optional(),
|
|
308
|
-
workspaceId: z.string().optional(),
|
|
309
|
-
channelId: z.string().optional(),
|
|
310
|
-
authorizedUserId: z.string().optional(),
|
|
311
|
-
})
|
|
312
|
-
.passthrough()
|
|
313
|
-
.optional(),
|
|
314
|
-
redact: z.boolean().optional(),
|
|
315
|
-
verbosity: z.enum(["lean", "verbose"]).optional(),
|
|
316
|
-
})
|
|
317
|
-
.passthrough()
|
|
318
|
-
.optional(),
|
|
319
|
-
})
|
|
320
|
-
.passthrough();
|
|
482
|
+
/** Read typed global-only notification config from a narrow settings reader. */
|
|
483
|
+
export function getNotificationConfig(settings: NotificationSettingsReader): NotificationConfig {
|
|
484
|
+
return notificationConfigFromSnapshot(settings.getNotificationSettingsSnapshot());
|
|
485
|
+
}
|
|
321
486
|
|
|
487
|
+
const notificationConfigSchema = z.object({}).passthrough();
|
|
322
488
|
type NotificationConfigFile = z.infer<typeof notificationConfigSchema>;
|
|
323
489
|
|
|
324
490
|
/** Read daemon settings through the canonical validated config-file boundary. */
|
|
@@ -326,119 +492,236 @@ export function loadNotificationConfigFile(agentDir: string): LoadResult<Notific
|
|
|
326
492
|
return new ConfigFile("config", notificationConfigSchema, path.join(agentDir, "config.yml")).tryLoad();
|
|
327
493
|
}
|
|
328
494
|
|
|
329
|
-
export function notificationConfigFromFile(
|
|
330
|
-
value
|
|
331
|
-
): Pick<NotificationConfig, "enabled" | "discord" | "slack" | "redact" | "verbosity"> {
|
|
332
|
-
const notifications = value.notifications;
|
|
333
|
-
return {
|
|
334
|
-
enabled: notifications?.enabled ?? false,
|
|
335
|
-
discord: notifications?.discord ?? {},
|
|
336
|
-
slack: notifications?.slack ?? {},
|
|
337
|
-
redact: notifications?.redact ?? false,
|
|
338
|
-
verbosity: notifications?.verbosity ?? "lean",
|
|
339
|
-
};
|
|
495
|
+
export function notificationConfigFromFile(value: NotificationConfigFile): NotificationConfig {
|
|
496
|
+
return notificationConfigFromSnapshot(parseNotificationSettingsSnapshot(value));
|
|
340
497
|
}
|
|
341
498
|
|
|
342
499
|
export function hasNonBlankValue(value: string | undefined): boolean {
|
|
343
500
|
return typeof value === "string" && value.trim().length > 0;
|
|
344
501
|
}
|
|
345
502
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
503
|
+
function providerRequiredFields(
|
|
504
|
+
cfg: NotificationConfig,
|
|
505
|
+
provider: NotificationProvider,
|
|
506
|
+
): Readonly<Record<string, unknown>> {
|
|
507
|
+
if (provider === "telegram") return { botToken: cfg.botToken, chatId: cfg.chatId };
|
|
508
|
+
if (provider === "discord") {
|
|
509
|
+
return {
|
|
510
|
+
botToken: cfg.discord.botToken,
|
|
511
|
+
applicationId: cfg.discord.applicationId,
|
|
512
|
+
guildId: cfg.discord.guildId,
|
|
513
|
+
parentChannelId: cfg.discord.parentChannelId,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
botToken: cfg.slack.botToken,
|
|
518
|
+
appToken: cfg.slack.appToken,
|
|
519
|
+
workspaceId: cfg.slack.workspaceId,
|
|
520
|
+
channelId: cfg.slack.channelId,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function providerOptionalFields(
|
|
525
|
+
cfg: NotificationConfig,
|
|
526
|
+
provider: NotificationProvider,
|
|
527
|
+
): Readonly<Record<string, unknown>> {
|
|
528
|
+
return provider === "slack" ? { authorizedUserId: cfg.slack.authorizedUserId } : {};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function providerConfigEnabled(cfg: NotificationConfig, provider: NotificationProvider): unknown {
|
|
532
|
+
if (provider === "telegram") return cfg.telegram?.enabled;
|
|
533
|
+
if (provider === "discord") return cfg.discord.enabled;
|
|
534
|
+
return cfg.slack.enabled;
|
|
359
535
|
}
|
|
360
536
|
|
|
361
|
-
|
|
362
|
-
|
|
537
|
+
function providerStoredIssues(
|
|
538
|
+
cfg: NotificationConfig,
|
|
539
|
+
provider: NotificationProvider,
|
|
540
|
+
): readonly ProviderResolutionIssue[] {
|
|
541
|
+
return cfg.providerIssues?.[provider] ?? [];
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function uniqueProviderIssues(issues: readonly ProviderResolutionIssue[]): ProviderResolutionIssue[] {
|
|
545
|
+
const result: ProviderResolutionIssue[] = [];
|
|
546
|
+
for (const issue of issues) {
|
|
547
|
+
if (!result.some(existing => existing.path === issue.path && existing.code === issue.code))
|
|
548
|
+
result.push({ ...issue });
|
|
549
|
+
}
|
|
550
|
+
return result;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function providerFacts(
|
|
554
|
+
cfg: NotificationConfig,
|
|
555
|
+
provider: NotificationProvider,
|
|
556
|
+
): {
|
|
557
|
+
configured: boolean;
|
|
558
|
+
quarantined: boolean;
|
|
559
|
+
issues: ProviderResolutionIssue[];
|
|
560
|
+
} {
|
|
561
|
+
const required = providerRequiredFields(cfg, provider);
|
|
562
|
+
const optional = providerOptionalFields(cfg, provider);
|
|
563
|
+
const issues = [...providerStoredIssues(cfg, provider)];
|
|
564
|
+
const anyRequiredPresent = Object.values(required).some(value => value !== undefined);
|
|
565
|
+
const explicitlyEnabled = providerConfigEnabled(cfg, provider) === true;
|
|
566
|
+
for (const [key, value] of Object.entries(required)) {
|
|
567
|
+
const pathName = `notifications.${provider}.${key}`;
|
|
568
|
+
if (value === undefined) {
|
|
569
|
+
if (anyRequiredPresent || explicitlyEnabled) issues.push({ path: pathName, code: "missing" });
|
|
570
|
+
} else if (typeof value !== "string") issues.push({ path: pathName, code: "wrong_type" });
|
|
571
|
+
else if (value.trim().length === 0) issues.push({ path: pathName, code: "blank" });
|
|
572
|
+
}
|
|
573
|
+
for (const [key, value] of Object.entries(optional)) {
|
|
574
|
+
if (value === undefined) continue;
|
|
575
|
+
const pathName = `notifications.${provider}.${key}`;
|
|
576
|
+
if (typeof value !== "string") issues.push({ path: pathName, code: "wrong_type" });
|
|
577
|
+
else if (value.trim().length === 0) issues.push({ path: pathName, code: "blank" });
|
|
578
|
+
}
|
|
579
|
+
const uniqueIssues = uniqueProviderIssues(issues);
|
|
580
|
+
const configured = Object.values(required).every(value => typeof value === "string" && value.trim().length > 0);
|
|
581
|
+
const quarantined = uniqueIssues.some(issue => issue.code !== "missing");
|
|
582
|
+
return { configured, quarantined, issues: uniqueIssues };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function isProviderComplete(cfg: NotificationConfig, provider: NotificationProvider): boolean {
|
|
586
|
+
return providerFacts(cfg, provider).configured;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function isTelegramComplete(
|
|
363
590
|
cfg: NotificationConfig,
|
|
364
591
|
): cfg is NotificationConfig & { botToken: string; chatId: string } {
|
|
365
|
-
return
|
|
592
|
+
return isProviderComplete(cfg, "telegram");
|
|
366
593
|
}
|
|
367
594
|
|
|
368
|
-
|
|
369
|
-
export function isDiscordConfigured(cfg: Pick<NotificationConfig, "enabled" | "discord">): cfg is Pick<
|
|
370
|
-
NotificationConfig,
|
|
371
|
-
"enabled"
|
|
372
|
-
> & {
|
|
595
|
+
export function isDiscordComplete(cfg: NotificationConfig): cfg is NotificationConfig & {
|
|
373
596
|
discord: { botToken: string; applicationId: string; guildId: string; parentChannelId: string };
|
|
374
597
|
} {
|
|
375
|
-
return (
|
|
376
|
-
cfg.enabled &&
|
|
377
|
-
hasNonBlankValue(cfg.discord.botToken) &&
|
|
378
|
-
hasNonBlankValue(cfg.discord.applicationId) &&
|
|
379
|
-
hasNonBlankValue(cfg.discord.guildId) &&
|
|
380
|
-
hasNonBlankValue(cfg.discord.parentChannelId)
|
|
381
|
-
);
|
|
598
|
+
return isProviderComplete(cfg, "discord");
|
|
382
599
|
}
|
|
383
600
|
|
|
384
|
-
|
|
385
|
-
export function isSlackConfigured(cfg: Pick<NotificationConfig, "enabled" | "slack">): cfg is Pick<
|
|
386
|
-
NotificationConfig,
|
|
387
|
-
"enabled"
|
|
388
|
-
> & {
|
|
601
|
+
export function isSlackComplete(cfg: NotificationConfig): cfg is NotificationConfig & {
|
|
389
602
|
slack: { botToken: string; appToken: string; workspaceId: string; channelId: string };
|
|
390
603
|
} {
|
|
391
|
-
return (
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
604
|
+
return isProviderComplete(cfg, "slack");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function resolveNotificationProvider(
|
|
608
|
+
cfg: NotificationConfig,
|
|
609
|
+
provider: NotificationProvider,
|
|
610
|
+
): ProviderResolution {
|
|
611
|
+
const facts = providerFacts(cfg, provider);
|
|
612
|
+
const configuredEnabled = providerConfigEnabled(cfg, provider);
|
|
613
|
+
const explicitIssue = facts.issues.some(issue => issue.path === `notifications.${provider}.enabled`);
|
|
614
|
+
const explicit = typeof configuredEnabled === "boolean" ? configuredEnabled : undefined;
|
|
615
|
+
const desiredSource: ProviderResolution["desiredSource"] =
|
|
616
|
+
explicit !== undefined || explicitIssue ? "explicit" : "legacy";
|
|
617
|
+
const desiredEnabled = explicitIssue ? false : (explicit ?? (facts.configured && !facts.quarantined));
|
|
618
|
+
return {
|
|
619
|
+
provider,
|
|
620
|
+
configured: facts.configured,
|
|
621
|
+
quarantined: facts.quarantined,
|
|
622
|
+
desiredEnabled,
|
|
623
|
+
desiredSource,
|
|
624
|
+
effectiveEnabled: cfg.enabled && desiredEnabled && facts.configured && !facts.quarantined,
|
|
625
|
+
issues: facts.issues,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export function isProviderEffectivelyEnabled(cfg: NotificationConfig, provider: NotificationProvider): boolean {
|
|
630
|
+
return resolveNotificationProvider(cfg, provider).effectiveEnabled;
|
|
398
631
|
}
|
|
399
632
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
633
|
+
export function hasAnyCompleteProvider(cfg: NotificationConfig): boolean {
|
|
634
|
+
return NOTIFICATION_PROVIDERS.some(provider => isProviderComplete(cfg, provider));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export function hasAnyEffectivelyEnabledProvider(cfg: NotificationConfig): boolean {
|
|
638
|
+
return NOTIFICATION_PROVIDERS.some(provider => isProviderEffectivelyEnabled(cfg, provider));
|
|
403
639
|
}
|
|
404
640
|
|
|
405
641
|
/**
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* `GJC_NOTIFY=off` (also `0` / `false`, case-insensitive) suppresses the
|
|
409
|
-
* completion notification surface for this process only. `config.yml` is
|
|
410
|
-
* untouched and child processes inherit the env var, which lets non-interactive
|
|
411
|
-
* fleet runs (`gjc -p --no-session`) stay silent even when a user-level/global
|
|
412
|
-
* completion notification configuration is enabled.
|
|
642
|
+
* Resolve generic live-stream policy. This policy only governs automatic
|
|
643
|
+
* current-session frames; it never changes durable provider eligibility.
|
|
413
644
|
*/
|
|
645
|
+
export type GenericNotificationSessionSource =
|
|
646
|
+
| "hard_opt_out"
|
|
647
|
+
| "session_local_off"
|
|
648
|
+
| "explicit_env"
|
|
649
|
+
| "token_env"
|
|
650
|
+
| "configured_provider"
|
|
651
|
+
| "session_scope"
|
|
652
|
+
| "none";
|
|
653
|
+
|
|
654
|
+
export interface GenericNotificationSessionEligibility {
|
|
655
|
+
enabled: boolean;
|
|
656
|
+
source: GenericNotificationSessionSource;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export type GenericNotificationStreamSource =
|
|
660
|
+
| "session_not_admitted"
|
|
661
|
+
| "env_on"
|
|
662
|
+
| "env_off"
|
|
663
|
+
| "durable_telegram"
|
|
664
|
+
| "none";
|
|
665
|
+
|
|
666
|
+
export interface GenericNotificationStreamPolicy {
|
|
667
|
+
enabled: boolean;
|
|
668
|
+
source: GenericNotificationStreamSource;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export interface GenericNotificationSessionEligibilityInput {
|
|
672
|
+
cfg: NotificationConfig;
|
|
673
|
+
env: NodeJS.ProcessEnv;
|
|
674
|
+
sessionDisabled: boolean;
|
|
675
|
+
spawnedByGjc?: boolean;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export function resolveGenericNotificationSessionEligibility(
|
|
679
|
+
input: GenericNotificationSessionEligibilityInput,
|
|
680
|
+
): GenericNotificationSessionEligibility {
|
|
681
|
+
if (input.env.GJC_NOTIFICATIONS === "0") return { enabled: false, source: "hard_opt_out" };
|
|
682
|
+
if (input.sessionDisabled) return { enabled: false, source: "session_local_off" };
|
|
683
|
+
if (input.env.GJC_NOTIFICATIONS === "1") return { enabled: true, source: "explicit_env" };
|
|
684
|
+
if (input.env.GJC_NOTIFICATIONS_TOKEN) return { enabled: true, source: "token_env" };
|
|
685
|
+
if (input.spawnedByGjc && input.cfg.sessionScope === "primary") {
|
|
686
|
+
return { enabled: false, source: "session_scope" };
|
|
687
|
+
}
|
|
688
|
+
if (hasAnyEffectivelyEnabledProvider(input.cfg)) return { enabled: true, source: "configured_provider" };
|
|
689
|
+
return { enabled: false, source: "none" };
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export function resolveGenericNotificationStreamPolicy(input: {
|
|
693
|
+
cfg: NotificationConfig;
|
|
694
|
+
env: NodeJS.ProcessEnv;
|
|
695
|
+
genericSessionEnabled: boolean;
|
|
696
|
+
}): GenericNotificationStreamPolicy {
|
|
697
|
+
if (!input.genericSessionEnabled) return { enabled: false, source: "session_not_admitted" };
|
|
698
|
+
const override = input.env.GJC_NOTIFICATIONS_STREAM?.trim().toLowerCase();
|
|
699
|
+
if (override === "1") return { enabled: true, source: "env_on" };
|
|
700
|
+
if (override === "0" || override === "off" || override === "false") return { enabled: false, source: "env_off" };
|
|
701
|
+
const durableEnabled =
|
|
702
|
+
input.cfg.streaming.enabled &&
|
|
703
|
+
isProviderEffectivelyEnabled(input.cfg, "telegram") &&
|
|
704
|
+
!getCurrentTelegramActivationMarker(input.cfg);
|
|
705
|
+
return { enabled: durableEnabled, source: durableEnabled ? "durable_telegram" : "none" };
|
|
706
|
+
}
|
|
707
|
+
|
|
414
708
|
export function completionNotifyDisabledByEnv(env: NodeJS.ProcessEnv): boolean {
|
|
415
|
-
const
|
|
416
|
-
return
|
|
709
|
+
const value = env.GJC_NOTIFY?.trim().toLowerCase();
|
|
710
|
+
return value === "off" || value === "0" || value === "false";
|
|
417
711
|
}
|
|
418
712
|
|
|
419
|
-
/** Canonical host eligibility for the dormant notification session surface. */
|
|
420
713
|
export interface NotificationHostEligibilityInput {
|
|
421
714
|
env: NodeJS.ProcessEnv;
|
|
422
|
-
/** False for host modes that cannot own a notification session endpoint. */
|
|
423
715
|
hostModeSupported?: boolean;
|
|
424
|
-
/** Task recursion depth; helper/subagent sessions must not own remote surfaces. */
|
|
425
716
|
taskDepth?: number;
|
|
426
|
-
/** Parent subagent id/prefix; present for helper/subagent sessions even when depth is omitted. */
|
|
427
717
|
parentTaskPrefix?: string;
|
|
428
|
-
/** Role-agent type/name; present for task sessions even if depth metadata is lost. */
|
|
429
718
|
currentAgentType?: string;
|
|
430
|
-
/** Canonical global session scope; absent preserves the default `all` behavior. */
|
|
431
719
|
sessionScope?: NotificationConfig["sessionScope"];
|
|
432
|
-
/** Whether this process was spawned by one of GJC's marked child spawn sites. */
|
|
433
720
|
spawnedByGjc?: boolean;
|
|
434
721
|
}
|
|
435
722
|
|
|
436
|
-
/**
|
|
437
|
-
|
|
438
|
-
* This intentionally says nothing about whether an adapter is configured: an
|
|
439
|
-
* eligible unconfigured host still gets a zero-side-effect control surface.
|
|
440
|
-
*/
|
|
441
|
-
export function isNotificationHostEligible(input: NotificationHostEligibilityInput): boolean {
|
|
723
|
+
/** Generic host eligibility for the dormant automatic notification surface. */
|
|
724
|
+
export function isGenericNotificationHostEligible(input: NotificationHostEligibilityInput): boolean {
|
|
442
725
|
if (completionNotifyDisabledByEnv(input.env)) return false;
|
|
443
726
|
if (input.hostModeSupported === false) return false;
|
|
444
727
|
if ((input.taskDepth ?? 0) > 0 || input.parentTaskPrefix || input.currentAgentType) return false;
|
|
@@ -448,31 +731,19 @@ export function isNotificationHostEligible(input: NotificationHostEligibilityInp
|
|
|
448
731
|
return true;
|
|
449
732
|
}
|
|
450
733
|
|
|
451
|
-
|
|
452
|
-
* Legacy compatibility helper for callers that require both host eligibility
|
|
453
|
-
* and a currently configured or explicit notification runtime.
|
|
454
|
-
*/
|
|
455
|
-
export function shouldRegisterNotificationsExtension(input: {
|
|
734
|
+
export interface GenericNotificationRegistrationInput {
|
|
456
735
|
env: NodeJS.ProcessEnv;
|
|
457
736
|
cfg?: NotificationConfig;
|
|
458
|
-
/** Task recursion depth; helper/subagent sessions must not spawn remote surfaces. */
|
|
459
737
|
taskDepth?: number;
|
|
460
|
-
/** Parent subagent id/prefix; present for helper/subagent sessions even when depth is omitted. */
|
|
461
738
|
parentTaskPrefix?: string;
|
|
462
|
-
/** Role-agent type/name; present for task sessions even if depth metadata is lost. */
|
|
463
739
|
currentAgentType?: string;
|
|
464
|
-
/**
|
|
465
|
-
* True when this session was launched by one of GJC's own programmatic
|
|
466
|
-
* separate-process child spawn sites (marked via {@link SPAWN_PROVENANCE_ENV}).
|
|
467
|
-
* Under `notifications.sessionScope = "primary"` such children are suppressed
|
|
468
|
-
* unless they explicitly opt in, so an interactive parent that fans out work
|
|
469
|
-
* does not flood the paired chat with topics for children the user never
|
|
470
|
-
* asked for. User-opened sessions (CLI/tmux/headless) never carry the marker.
|
|
471
|
-
*/
|
|
472
740
|
spawnedByGjc?: boolean;
|
|
473
|
-
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** Generic registration admission; direct provider actions do not call this helper. */
|
|
744
|
+
export function shouldRegisterGenericNotificationsExtension(input: GenericNotificationRegistrationInput): boolean {
|
|
474
745
|
if (
|
|
475
|
-
!
|
|
746
|
+
!isGenericNotificationHostEligible({
|
|
476
747
|
env: input.env,
|
|
477
748
|
taskDepth: input.taskDepth,
|
|
478
749
|
parentTaskPrefix: input.parentTaskPrefix,
|
|
@@ -483,35 +754,12 @@ export function shouldRegisterNotificationsExtension(input: {
|
|
|
483
754
|
) {
|
|
484
755
|
return false;
|
|
485
756
|
}
|
|
486
|
-
return
|
|
487
|
-
|
|
488
|
-
Boolean(input.env.GJC_NOTIFICATIONS_TOKEN) ||
|
|
489
|
-
Boolean(input.cfg && isGloballyConfigured(input.cfg))
|
|
490
|
-
);
|
|
757
|
+
if (input.env.GJC_NOTIFICATIONS === "1" || input.env.GJC_NOTIFICATIONS_TOKEN) return true;
|
|
758
|
+
return input.cfg !== undefined && hasAnyEffectivelyEnabledProvider(input.cfg);
|
|
491
759
|
}
|
|
492
760
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
* Precedence (highest first):
|
|
496
|
-
* 1) env.GJC_NOTIFICATIONS === "0" -> false (hard opt-out)
|
|
497
|
-
* 2) sessionDisabled === true -> false (local /notify off)
|
|
498
|
-
* 3) env.GJC_NOTIFICATIONS === "1" || env.GJC_NOTIFICATIONS_TOKEN present -> true (legacy explicit)
|
|
499
|
-
* 4) spawned GJC child with `sessionScope=primary` -> false
|
|
500
|
-
* 5) isGloballyConfigured(cfg) -> true (global auto-on)
|
|
501
|
-
* 6) otherwise false
|
|
502
|
-
*/
|
|
503
|
-
export function isSessionNotificationsEnabled(input: {
|
|
504
|
-
cfg: NotificationConfig;
|
|
505
|
-
env: NodeJS.ProcessEnv;
|
|
506
|
-
sessionDisabled: boolean;
|
|
507
|
-
/** This process was programmatically spawned by GJC (consumed at SDK startup). */
|
|
508
|
-
spawnedByGjc?: boolean;
|
|
509
|
-
}): boolean {
|
|
510
|
-
if (input.env.GJC_NOTIFICATIONS === "0") return false;
|
|
511
|
-
if (input.sessionDisabled) return false;
|
|
512
|
-
if (input.env.GJC_NOTIFICATIONS === "1" || input.env.GJC_NOTIFICATIONS_TOKEN) return true;
|
|
513
|
-
if (input.spawnedByGjc && input.cfg.sessionScope === "primary") return false;
|
|
514
|
-
return isGloballyConfigured(input.cfg);
|
|
761
|
+
export function isGenericNotificationSessionEnabled(input: GenericNotificationSessionEligibilityInput): boolean {
|
|
762
|
+
return resolveGenericNotificationSessionEligibility(input).enabled;
|
|
515
763
|
}
|
|
516
764
|
|
|
517
765
|
/** Mask a bot token for display: first 4 chars + "…" + "(len N)"; "(unset)" when undefined/empty. Never reveal full token. */
|