@sentry/junior 0.66.2 → 0.67.0

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.
@@ -8,9 +8,10 @@ import {
8
8
  getStateAdapter,
9
9
  parseSlackThreadId,
10
10
  sandboxSkillDir
11
- } from "./chunk-SAYCFF7O.js";
11
+ } from "./chunk-NWU2Z6SM.js";
12
12
  import {
13
13
  isRecord,
14
+ logException,
14
15
  logInfo,
15
16
  logWarn
16
17
  } from "./chunk-6QWWMZCK.js";
@@ -23,755 +24,1788 @@ import {
23
24
  worldPathCandidates
24
25
  } from "./chunk-KVZL5NZS.js";
25
26
 
26
- // src/handlers/health.ts
27
- function GET() {
28
- return Response.json({
29
- status: "ok",
30
- service: "junior",
31
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
32
- });
27
+ // src/chat/plugins/logging.ts
28
+ function createAgentPluginLogger(plugin) {
29
+ return {
30
+ info(message, metadata) {
31
+ logInfo(
32
+ "agent_plugin_log_info",
33
+ {},
34
+ { "app.plugin.name": plugin, ...metadata },
35
+ message
36
+ );
37
+ },
38
+ warn(message, metadata) {
39
+ logWarn(
40
+ "agent_plugin_log_warn",
41
+ {},
42
+ { "app.plugin.name": plugin, ...metadata },
43
+ message
44
+ );
45
+ },
46
+ error(message, metadata) {
47
+ logException(
48
+ new Error(message),
49
+ "agent_plugin_log_error",
50
+ {},
51
+ { "app.plugin.name": plugin, ...metadata },
52
+ message
53
+ );
54
+ }
55
+ };
33
56
  }
34
57
 
35
- // src/chat/prompt.ts
36
- import fs from "fs";
37
- import path from "path";
38
-
39
- // src/chat/turn-context-tag.ts
40
- var TURN_CONTEXT_TAG = "runtime-turn-context";
41
-
42
- // src/chat/interruption-marker.ts
43
- var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
44
- function getInterruptionMarker() {
45
- return INTERRUPTED_MARKER;
58
+ // src/chat/plugins/state.ts
59
+ import { createHash } from "crypto";
60
+ var MAX_PLUGIN_STATE_KEY_LENGTH = 512;
61
+ function hashKeyPart(value) {
62
+ return createHash("sha256").update(value).digest("hex").slice(0, 32);
46
63
  }
47
-
48
- // src/chat/slack/status-format.ts
49
- var SLACK_STATUS_MAX_LENGTH = 50;
50
- function truncateStatusText(text) {
51
- const trimmed = text.trim();
52
- if (!trimmed) {
53
- return "";
64
+ function pluginStateKey(plugin, key2) {
65
+ return `junior:plugin_state:${hashKeyPart(plugin)}:${hashKeyPart(key2)}`;
66
+ }
67
+ function validatePluginStateKey(key2) {
68
+ if (!key2.trim()) {
69
+ throw new Error("Plugin state key is required");
54
70
  }
55
- if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) {
56
- return trimmed;
71
+ if (key2.length > MAX_PLUGIN_STATE_KEY_LENGTH) {
72
+ throw new Error("Plugin state key exceeds the maximum length");
57
73
  }
58
- return `${trimmed.slice(0, SLACK_STATUS_MAX_LENGTH - 3).trimEnd()}...`;
59
74
  }
60
-
61
- // src/chat/slack/mrkdwn.ts
62
- function ensureBlockSpacing(text) {
63
- const codeBlockPattern = /^```/;
64
- const listItemPattern = /^[-*•]\s|^\d+\.\s/;
65
- const lines = text.split("\n");
66
- const result = [];
67
- let inCodeBlock = false;
68
- for (let i = 0; i < lines.length; i++) {
69
- const line = lines[i];
70
- const isCodeFence = codeBlockPattern.test(line.trimStart());
71
- if (isCodeFence) {
72
- if (!inCodeBlock) {
73
- const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
74
- if (prev2 !== void 0 && prev2.trim() !== "") {
75
- result.push("");
76
- }
77
- }
78
- inCodeBlock = !inCodeBlock;
79
- result.push(line);
80
- continue;
81
- }
82
- if (inCodeBlock) {
83
- result.push(line);
75
+ function legacyStateKey(key2, options) {
76
+ for (const prefix of options?.legacyStatePrefixes ?? []) {
77
+ const trimmed = prefix.trim();
78
+ if (!trimmed) {
84
79
  continue;
85
80
  }
86
- const prev = result.length > 0 ? result[result.length - 1] : void 0;
87
- if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
88
- result.push("");
81
+ if (key2 === trimmed || key2.startsWith(`${trimmed}:`)) {
82
+ return key2;
89
83
  }
90
- result.push(line);
91
84
  }
92
- return result.join("\n");
93
- }
94
- function renderSlackMrkdwn(text) {
95
- let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
96
- normalized = ensureBlockSpacing(normalized);
97
- return normalized.replace(/\n{3,}/g, "\n\n").trim();
85
+ return void 0;
98
86
  }
99
- function normalizeSlackStatusText(text) {
100
- const trimmed = text.trim();
101
- if (!trimmed) {
102
- return "";
103
- }
104
- return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
87
+ function createPluginState(plugin, options) {
88
+ return {
89
+ async delete(key2) {
90
+ validatePluginStateKey(key2);
91
+ const state = getStateAdapter();
92
+ await state.connect();
93
+ await state.delete(pluginStateKey(plugin, key2));
94
+ const legacyKey = legacyStateKey(key2, options);
95
+ if (legacyKey) {
96
+ await state.delete(legacyKey);
97
+ }
98
+ },
99
+ async get(key2) {
100
+ validatePluginStateKey(key2);
101
+ const state = getStateAdapter();
102
+ await state.connect();
103
+ const value = await state.get(pluginStateKey(plugin, key2));
104
+ if (value !== null && value !== void 0) {
105
+ return value;
106
+ }
107
+ const legacyKey = legacyStateKey(key2, options);
108
+ return legacyKey ? await state.get(legacyKey) ?? void 0 : void 0;
109
+ },
110
+ async set(key2, value, ttlMs) {
111
+ validatePluginStateKey(key2);
112
+ const state = getStateAdapter();
113
+ await state.connect();
114
+ await state.set(pluginStateKey(plugin, key2), value, ttlMs);
115
+ },
116
+ async setIfNotExists(key2, value, ttlMs) {
117
+ validatePluginStateKey(key2);
118
+ const state = getStateAdapter();
119
+ await state.connect();
120
+ const legacyKey = legacyStateKey(key2, options);
121
+ if (legacyKey) {
122
+ const existing = await state.get(legacyKey);
123
+ if (existing !== null && existing !== void 0) {
124
+ return false;
125
+ }
126
+ }
127
+ return await state.setIfNotExists(
128
+ pluginStateKey(plugin, key2),
129
+ value,
130
+ ttlMs
131
+ );
132
+ },
133
+ async withLock(key2, ttlMs, callback) {
134
+ validatePluginStateKey(key2);
135
+ const state = getStateAdapter();
136
+ await state.connect();
137
+ const lockKey = legacyStateKey(key2, options) ?? pluginStateKey(plugin, key2);
138
+ const lock = await state.acquireLock(lockKey, ttlMs);
139
+ if (!lock) {
140
+ throw new Error(`Could not acquire plugin state lock for ${key2}`);
141
+ }
142
+ try {
143
+ return await callback();
144
+ } finally {
145
+ await state.releaseLock(lock);
146
+ }
147
+ }
148
+ };
105
149
  }
106
150
 
107
- // src/chat/slack/output.ts
108
- var MAX_INLINE_CHARS = 2200;
109
- var MAX_INLINE_LINES = 45;
110
- var CONTINUED_MARKER = "\n\n[Continued below]";
111
- function countSlackLines(text) {
112
- if (!text) {
113
- return 0;
151
+ // src/chat/credentials/subject.ts
152
+ import { createHmac, timingSafeEqual } from "crypto";
153
+
154
+ // src/chat/slack/client.ts
155
+ import { WebClient } from "@slack/web-api";
156
+ var SlackActionError = class extends Error {
157
+ code;
158
+ apiError;
159
+ needed;
160
+ provided;
161
+ statusCode;
162
+ requestId;
163
+ errorData;
164
+ retryAfterSeconds;
165
+ detail;
166
+ detailLine;
167
+ detailRule;
168
+ constructor(message, code, options = {}) {
169
+ super(message);
170
+ this.name = "SlackActionError";
171
+ this.code = code;
172
+ this.apiError = options.apiError;
173
+ this.needed = options.needed;
174
+ this.provided = options.provided;
175
+ this.statusCode = options.statusCode;
176
+ this.requestId = options.requestId;
177
+ this.errorData = options.errorData;
178
+ this.retryAfterSeconds = options.retryAfterSeconds;
179
+ this.detail = options.detail;
180
+ this.detailLine = options.detailLine;
181
+ this.detailRule = options.detailRule;
114
182
  }
115
- return text.split("\n").length;
116
- }
117
- function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
118
- return text.length <= maxChars && countSlackLines(text) <= maxLines;
119
- }
120
- function findSplitIndex(text, maxChars) {
121
- if (text.length <= maxChars) {
122
- return text.length;
183
+ };
184
+ function serializeSlackErrorData(data) {
185
+ if (!data || typeof data !== "object") {
186
+ return void 0;
123
187
  }
124
- const bounded = text.slice(0, maxChars);
125
- const newlineIndex = bounded.lastIndexOf("\n");
126
- if (newlineIndex > 0) {
127
- return newlineIndex;
188
+ const filtered = Object.fromEntries(
189
+ Object.entries(data).filter(
190
+ ([key2]) => key2 !== "error"
191
+ )
192
+ );
193
+ if (Object.keys(filtered).length === 0) {
194
+ return void 0;
128
195
  }
129
- const spaceIndex = bounded.lastIndexOf(" ");
130
- if (spaceIndex > 0) {
131
- return spaceIndex;
196
+ try {
197
+ const serialized = JSON.stringify(filtered);
198
+ return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
199
+ } catch {
200
+ return void 0;
132
201
  }
133
- return maxChars;
134
202
  }
135
- function splitByLineBudget(text, maxLines) {
136
- if (maxLines <= 0) {
137
- return "";
203
+ function getHeaderString(headers, name) {
204
+ if (!headers || typeof headers !== "object") {
205
+ return void 0;
138
206
  }
139
- const lines = text.split("\n");
140
- if (lines.length <= maxLines) {
141
- return text;
207
+ const key2 = name.toLowerCase();
208
+ const entries = headers;
209
+ for (const [entryKey, value] of Object.entries(entries)) {
210
+ if (entryKey.toLowerCase() !== key2) continue;
211
+ if (typeof value === "string") return value;
212
+ if (Array.isArray(value)) {
213
+ const first = value.find((entry) => typeof entry === "string");
214
+ return typeof first === "string" ? first : void 0;
215
+ }
142
216
  }
143
- return lines.slice(0, maxLines).join("\n");
144
- }
145
- function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
146
- return {
147
- maxChars: Math.max(1, maxChars - suffix.length),
148
- maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
149
- };
217
+ return void 0;
150
218
  }
151
- function forceSplitBudget(text, budget) {
152
- const lineCount = countSlackLines(text);
153
- return {
154
- maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
155
- maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
219
+ function parseSlackCanvasDetail(detail) {
220
+ if (typeof detail !== "string") {
221
+ return {};
222
+ }
223
+ const trimmed = detail.trim();
224
+ if (!trimmed) {
225
+ return {};
226
+ }
227
+ const parsed = {
228
+ detail: trimmed
156
229
  };
157
- }
158
- function getFenceContinuation(text) {
159
- let open;
160
- for (const line of text.split("\n")) {
161
- const trimmed = line.trimStart();
162
- const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
163
- if (!openerMatch) {
164
- continue;
165
- }
166
- if (!open) {
167
- open = {
168
- fence: openerMatch[1],
169
- openerLine: trimmed
170
- };
171
- continue;
172
- }
173
- if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
174
- open = void 0;
230
+ const lineMatch = trimmed.match(/line\s+(\d+):/i);
231
+ if (lineMatch) {
232
+ const line = Number.parseInt(lineMatch[1] ?? "", 10);
233
+ if (Number.isFinite(line)) {
234
+ parsed.detailLine = line;
175
235
  }
176
236
  }
177
- if (!open) {
178
- return null;
237
+ if (/unsupported heading depth/i.test(trimmed)) {
238
+ parsed.detailRule = "unsupported_heading_depth";
179
239
  }
180
- return {
181
- closeSuffix: text.endsWith("\n") ? open.fence : `
182
- ${open.fence}`,
183
- reopenPrefix: `${open.openerLine}
184
- `
185
- };
186
- }
187
- function appendSlackSuffix(text, marker) {
188
- const carryover = getFenceContinuation(text);
189
- return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
240
+ return parsed;
190
241
  }
191
- function stripTrailingContinuationMarker(text) {
192
- return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
242
+ var client = null;
243
+ function normalizeSlackConversationId(channelId) {
244
+ if (!channelId) return void 0;
245
+ const trimmed = channelId.trim();
246
+ if (!trimmed) return void 0;
247
+ if (!trimmed.startsWith("slack:")) {
248
+ return trimmed;
249
+ }
250
+ const parts = trimmed.split(":");
251
+ return parts[1]?.trim() || void 0;
193
252
  }
194
- function takeSlackContinuationChunk(text, budget) {
195
- let { prefix, rest } = takeSlackInlinePrefix(text, budget);
196
- if (!rest) {
197
- ({ prefix, rest } = takeSlackInlinePrefix(
198
- text,
199
- forceSplitBudget(text, budget)
200
- ));
253
+ function getClient() {
254
+ if (client) return client;
255
+ const token = getSlackBotToken();
256
+ if (!token) {
257
+ throw new SlackActionError(
258
+ "SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack canvas/list actions in this service",
259
+ "missing_token"
260
+ );
201
261
  }
202
- let carryover = rest ? getFenceContinuation(prefix) : null;
203
- if (!carryover) {
204
- return { prefix, rest };
262
+ client = new WebClient(token);
263
+ return client;
264
+ }
265
+ function mapSlackError(error) {
266
+ if (error instanceof SlackActionError) {
267
+ return error;
205
268
  }
206
- const carryoverBudget = reserveInlineBudgetForSuffix(
207
- `${carryover.closeSuffix}${CONTINUED_MARKER}`
208
- );
209
- ({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
210
- if (!rest) {
211
- ({ prefix, rest } = takeSlackInlinePrefix(
212
- text,
213
- forceSplitBudget(text, carryoverBudget)
214
- ));
269
+ const candidate = error;
270
+ const apiError = candidate.data?.error;
271
+ const message = candidate.message ?? "Slack action failed";
272
+ const baseOptions = {
273
+ apiError,
274
+ statusCode: candidate.statusCode,
275
+ requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
276
+ errorData: serializeSlackErrorData(candidate.data),
277
+ ...parseSlackCanvasDetail(candidate.data?.detail)
278
+ };
279
+ if (apiError === "missing_scope") {
280
+ return new SlackActionError(message, "missing_scope", {
281
+ ...baseOptions,
282
+ needed: candidate.data?.needed,
283
+ provided: candidate.data?.provided
284
+ });
215
285
  }
216
- carryover = rest ? getFenceContinuation(prefix) : null;
217
- if (!carryover) {
218
- return { prefix, rest };
286
+ if (apiError === "not_in_channel") {
287
+ return new SlackActionError(message, "not_in_channel", baseOptions);
219
288
  }
220
- return {
221
- prefix,
222
- rest: `${carryover.reopenPrefix}${rest}`
223
- };
224
- }
225
- function takeSlackContinuationPrefix(text, options) {
226
- const budget = {
227
- maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
228
- maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
229
- };
230
- const { prefix, rest } = (() => {
231
- if (options?.forceSplit) {
232
- return takeSlackContinuationChunk(text, budget);
233
- }
234
- const initial = takeSlackInlinePrefix(text, budget);
235
- return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
236
- })();
237
- return {
238
- prefix,
239
- renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
240
- rest
241
- };
242
- }
243
- function takeSlackInlinePrefix(text, options) {
244
- const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
245
- const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
246
- const normalized = text.replace(/\r\n?/g, "\n");
247
- if (!normalized) {
248
- return { prefix: "", rest: "" };
289
+ if (apiError === "already_reacted") {
290
+ return new SlackActionError(message, "already_reacted", baseOptions);
249
291
  }
250
- if (fitsInlineBudget(normalized, maxChars, maxLines)) {
251
- return { prefix: normalized, rest: "" };
292
+ if (apiError === "no_reaction") {
293
+ return new SlackActionError(message, "no_reaction", baseOptions);
252
294
  }
253
- const lineBounded = splitByLineBudget(normalized, maxLines);
254
- const cutIndex = findSplitIndex(lineBounded, maxChars);
255
- const prefix = lineBounded.slice(0, cutIndex).trimEnd();
256
- if (prefix) {
257
- return {
258
- prefix,
259
- rest: normalized.slice(prefix.length).trimStart()
260
- };
295
+ if (apiError === "invalid_arguments") {
296
+ return new SlackActionError(message, "invalid_arguments", baseOptions);
261
297
  }
262
- const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
263
- return {
264
- prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
265
- rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
266
- };
267
- }
268
- function splitSlackReplyText(text, options) {
269
- const normalized = renderSlackMrkdwn(text);
270
- if (!normalized) {
271
- return [];
298
+ if (apiError === "invalid_cursor") {
299
+ return new SlackActionError(message, "invalid_arguments", baseOptions);
272
300
  }
273
- const chunks = [];
274
- const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
275
- let remaining = normalized;
276
- while (remaining) {
277
- const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
278
- if (fitsFinalChunk) {
279
- chunks.push(
280
- options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
281
- );
282
- break;
283
- }
284
- const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
285
- ...continuationBudget,
286
- forceSplit: true
287
- });
288
- chunks.push(renderedPrefix);
289
- remaining = rest;
301
+ if (apiError === "invalid_name") {
302
+ return new SlackActionError(message, "invalid_arguments", baseOptions);
290
303
  }
291
- if (chunks.length === 2) {
292
- chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
304
+ if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") {
305
+ return new SlackActionError(message, "not_found", baseOptions);
293
306
  }
294
- return chunks;
295
- }
296
- function getSlackContinuationBudget() {
297
- return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
298
- }
299
- function buildSlackOutputMessage(text, files) {
300
- const normalized = renderSlackMrkdwn(text);
301
- const fileCount = files?.length ?? 0;
302
- if (!normalized) {
303
- if (fileCount > 0) {
304
- return {
305
- raw: "",
306
- files
307
- };
308
- }
309
- throw new Error(
310
- `Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
311
- );
307
+ if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") {
308
+ return new SlackActionError(message, "feature_unavailable", baseOptions);
312
309
  }
313
- return {
314
- markdown: normalized,
315
- files
316
- };
317
- }
318
- var slackOutputPolicy = {
319
- maxInlineChars: MAX_INLINE_CHARS,
320
- maxInlineLines: MAX_INLINE_LINES
321
- };
322
-
323
- // src/chat/xml.ts
324
- function escapeXml(value) {
325
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
310
+ if (apiError === "canvas_creation_failed") {
311
+ return new SlackActionError(message, "canvas_creation_failed", baseOptions);
312
+ }
313
+ if (apiError === "canvas_editing_failed") {
314
+ return new SlackActionError(message, "canvas_editing_failed", baseOptions);
315
+ }
316
+ if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) {
317
+ return new SlackActionError(message, "rate_limited", {
318
+ ...baseOptions,
319
+ retryAfterSeconds: candidate.retryAfter
320
+ });
321
+ }
322
+ return new SlackActionError(message, "internal_error", baseOptions);
326
323
  }
327
-
328
- // src/chat/prompt.ts
329
- var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
330
- function getLoggedMarkdownFiles() {
331
- const globalState = globalThis;
332
- globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
333
- return globalState.__juniorLoggedMarkdownFiles;
324
+ function sleep(ms) {
325
+ return new Promise((resolve) => setTimeout(resolve, ms));
334
326
  }
335
- function loadOptionalMarkdownFile(candidates, fileName) {
336
- for (const resolved of candidates) {
327
+ async function withSlackRetries(task, maxAttempts = 3, context = {}) {
328
+ let attempt = 0;
329
+ while (attempt < maxAttempts) {
330
+ attempt += 1;
337
331
  try {
338
- const raw = fs.readFileSync(resolved, "utf8").trim();
339
- if (raw.length > 0) {
340
- const loggedMarkdownFiles = getLoggedMarkdownFiles();
341
- const logKey = `${fileName}:${resolved}`;
342
- if (!loggedMarkdownFiles.has(logKey)) {
343
- loggedMarkdownFiles.add(logKey);
344
- logInfo(
345
- `${fileName.toLowerCase()}_loaded`,
346
- {},
347
- {
348
- "file.path": resolved
349
- },
350
- `Loaded ${fileName}`
351
- );
352
- }
353
- return raw;
332
+ return await task();
333
+ } catch (error) {
334
+ const mapped = mapSlackError(error);
335
+ const isRetryable = mapped.code === "rate_limited";
336
+ const baseLogAttributes = {
337
+ "app.slack.action": context.action ?? "unknown",
338
+ "app.slack.error_code": mapped.code,
339
+ ...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : {},
340
+ ...mapped.detail ? { "app.slack.detail": mapped.detail } : {},
341
+ ...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : {},
342
+ ...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : {},
343
+ ...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : {},
344
+ ...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : {},
345
+ ...context.attributes ?? {}
346
+ };
347
+ if (!isRetryable || attempt >= maxAttempts) {
348
+ logWarn(
349
+ "slack_action_failed",
350
+ {},
351
+ {
352
+ ...baseLogAttributes,
353
+ ...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : {}
354
+ },
355
+ "Slack action failed"
356
+ );
357
+ throw mapped;
354
358
  }
355
- } catch {
356
- continue;
359
+ logWarn(
360
+ "slack_action_retrying",
361
+ {},
362
+ {
363
+ ...baseLogAttributes,
364
+ "app.slack.retry_attempt": attempt
365
+ },
366
+ "Retrying Slack action after transient failure"
367
+ );
368
+ const retryAfterMs = mapped.code === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
369
+ const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
370
+ await sleep(retryAfterMs ?? backoffMs);
357
371
  }
358
372
  }
359
- return null;
373
+ throw new SlackActionError(
374
+ "Slack action exhausted retries",
375
+ "internal_error"
376
+ );
360
377
  }
361
- function loadSoul() {
362
- const soul = loadOptionalMarkdownFile(soulPathCandidates(), "SOUL.md");
363
- if (soul) {
364
- return soul;
365
- }
366
- logWarn(
367
- "soul_load_fallback",
368
- {},
369
- {
370
- "app.file.candidates": soulPathCandidates()
371
- },
372
- "SOUL.md not found; using built-in default personality"
373
- );
374
- return DEFAULT_SOUL;
378
+ function getSlackClient() {
379
+ return getClient();
375
380
  }
376
- function loadWorld() {
377
- return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
381
+ function isDmChannel(channelId) {
382
+ const normalized = normalizeSlackConversationId(channelId);
383
+ return Boolean(normalized && normalized.startsWith("D"));
378
384
  }
379
- var JUNIOR_PERSONALITY = (() => {
380
- try {
381
- return loadSoul();
382
- } catch (error) {
383
- logWarn(
384
- "soul_load_failed",
385
- {},
386
- {
387
- "exception.message": error instanceof Error ? error.message : String(error)
388
- },
389
- "Failed to load SOUL.md; using built-in default personality"
385
+ function isConversationScopedChannel(channelId) {
386
+ const normalized = normalizeSlackConversationId(channelId);
387
+ if (!normalized) return false;
388
+ return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
389
+ }
390
+ function isConversationChannel(channelId) {
391
+ const normalized = normalizeSlackConversationId(channelId);
392
+ if (!normalized) return false;
393
+ return normalized.startsWith("C") || normalized.startsWith("G");
394
+ }
395
+ async function getFilePermalink(fileId) {
396
+ const client2 = getClient();
397
+ const response = await withSlackRetries(
398
+ () => client2.files.info({
399
+ file: fileId
400
+ })
401
+ );
402
+ return response.file?.permalink;
403
+ }
404
+ async function downloadPrivateSlackFile(url) {
405
+ const token = getSlackBotToken();
406
+ if (!token) {
407
+ throw new SlackActionError(
408
+ "SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack file downloads in this service",
409
+ "missing_token"
390
410
  );
391
- return DEFAULT_SOUL;
392
411
  }
393
- })();
394
- var JUNIOR_WORLD = (() => {
395
- try {
396
- return loadWorld();
397
- } catch (error) {
398
- logWarn(
399
- "world_load_failed",
400
- {},
401
- {
402
- "exception.message": error instanceof Error ? error.message : String(error)
403
- },
404
- "Failed to load WORLD.md; omitting world prompt context"
405
- );
406
- return null;
412
+ const response = await fetch(url, {
413
+ headers: {
414
+ Authorization: `Bearer ${token}`
415
+ }
416
+ });
417
+ if (!response.ok) {
418
+ throw new Error(`Slack file download failed: ${response.status}`);
407
419
  }
408
- })();
409
- function workspaceSkillDir(skillName) {
410
- return sandboxSkillDir(skillName);
420
+ return Buffer.from(await response.arrayBuffer());
411
421
  }
412
- function formatConfigurationValue(value) {
413
- if (typeof value === "string") {
414
- return escapeXml(value);
415
- }
416
- try {
417
- return escapeXml(JSON.stringify(value));
418
- } catch {
419
- return escapeXml(String(value));
420
- }
422
+
423
+ // src/chat/credentials/subject.ts
424
+ var CREDENTIAL_SUBJECT_HMAC_CONTEXT = "junior.credential_subject.v1";
425
+ var CREDENTIAL_SUBJECT_SIGNATURE_VERSION = "v1";
426
+ function getCredentialSubjectSecret() {
427
+ return process.env.JUNIOR_SECRET?.trim() || void 0;
421
428
  }
422
- function renderRequesterBlock(fields) {
423
- const lines = Object.entries(fields).filter(([, value]) => Boolean(value)).map(([key2, value]) => `- ${key2}: ${escapeXml(value)}`);
424
- if (lines.length === 0) {
425
- return null;
429
+ function buildPayload(input) {
430
+ return [
431
+ CREDENTIAL_SUBJECT_HMAC_CONTEXT,
432
+ input.allowedWhen,
433
+ input.teamId,
434
+ input.channelId,
435
+ input.userId
436
+ ].join("\0");
437
+ }
438
+ function signPayload(secret, payload) {
439
+ const digest = createHmac("sha256", secret).update(payload).digest("hex");
440
+ return `${CREDENTIAL_SUBJECT_SIGNATURE_VERSION}=${digest}`;
441
+ }
442
+ function timingSafeMatch(expected, actual) {
443
+ const expectedBuffer = Buffer.from(expected);
444
+ const actualBuffer = Buffer.from(actual);
445
+ if (expectedBuffer.length !== actualBuffer.length) {
446
+ return false;
447
+ }
448
+ return timingSafeEqual(expectedBuffer, actualBuffer);
449
+ }
450
+ function createSlackDirectCredentialSubject(input) {
451
+ const channelId = normalizeSlackConversationId(input.channelId);
452
+ const teamId = input.teamId?.trim();
453
+ const userId = input.userId?.trim();
454
+ if (!channelId || !teamId || !userId || !isDmChannel(channelId)) {
455
+ return void 0;
426
456
  }
427
- return ["<requester>", ...lines, "</requester>"];
428
- }
429
- function renderTag(tag, lines) {
430
- return [`<${tag}>`, ...lines, `</${tag}>`];
431
- }
432
- function renderTagBlock(tag, content) {
433
- return [`<${tag}>`, content, `</${tag}>`].join("\n");
457
+ return {
458
+ type: "user",
459
+ userId,
460
+ allowedWhen: "private-direct-conversation"
461
+ };
434
462
  }
435
- function formatSkillEntry(skill) {
436
- const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
437
- const lines = [];
438
- lines.push(" <skill>");
439
- lines.push(` <name>${escapeXml(skill.name)}</name>`);
440
- lines.push(` <description>${escapeXml(skill.description)}</description>`);
441
- lines.push(` <location>${escapeXml(skillLocation)}</location>`);
442
- lines.push(" </skill>");
443
- return lines;
463
+ function bindSlackDirectCredentialSubject(input) {
464
+ const channelId = normalizeSlackConversationId(input.channelId);
465
+ const teamId = input.teamId.trim();
466
+ const secret = getCredentialSubjectSecret();
467
+ const { subject } = input;
468
+ const userId = subject.userId.trim();
469
+ if (!channelId || !teamId || !secret || !isDmChannel(channelId) || subject.type !== "user" || !userId || subject.allowedWhen !== "private-direct-conversation") {
470
+ return void 0;
471
+ }
472
+ return {
473
+ type: "user",
474
+ userId,
475
+ allowedWhen: subject.allowedWhen,
476
+ binding: {
477
+ type: "slack-direct-conversation",
478
+ teamId,
479
+ channelId,
480
+ signature: signPayload(
481
+ secret,
482
+ buildPayload({
483
+ allowedWhen: subject.allowedWhen,
484
+ teamId,
485
+ channelId,
486
+ userId
487
+ })
488
+ )
489
+ }
490
+ };
444
491
  }
445
- function formatAvailableSkillsForPrompt(skills, invocation) {
446
- const autoSelectable = skills.filter(
447
- (s) => s.disableModelInvocation !== true
492
+ function verifySlackDirectCredentialSubject(input) {
493
+ const channelId = normalizeSlackConversationId(input.channelId);
494
+ const secret = getCredentialSubjectSecret();
495
+ if (!channelId || !secret) {
496
+ return false;
497
+ }
498
+ const { subject } = input;
499
+ const binding = subject.binding;
500
+ if (subject.type !== "user" || typeof subject.userId !== "string" || !subject.userId || subject.allowedWhen !== "private-direct-conversation" || !binding || binding.type !== "slack-direct-conversation" || typeof binding.signature !== "string" || !binding.signature || binding.teamId !== input.teamId || binding.channelId !== channelId) {
501
+ return false;
502
+ }
503
+ const expected = signPayload(
504
+ secret,
505
+ buildPayload({
506
+ allowedWhen: subject.allowedWhen,
507
+ teamId: binding.teamId,
508
+ channelId: binding.channelId,
509
+ userId: subject.userId
510
+ })
448
511
  );
449
- const invokedExplicitOnly = invocation ? skills.filter(
450
- (s) => s.disableModelInvocation === true && s.name === invocation.skillName
451
- ) : [];
452
- const sections = [];
453
- if (autoSelectable.length > 0) {
454
- const available = [
455
- "<available-skills>",
456
- "Scan before answering. Load the most specific matching skill; do not answer from memory when a skill fits. A request that names a skill, plugin, provider, or account matching a skill name is a skill match. If none fits, do not load a skill."
457
- ];
458
- for (const skill of autoSelectable) {
459
- available.push(...formatSkillEntry(skill));
460
- }
461
- available.push("</available-skills>");
462
- sections.push(available.join("\n"));
512
+ return timingSafeMatch(expected, binding.signature);
513
+ }
514
+
515
+ // src/chat/plugins/agent-hooks.ts
516
+ var AgentPluginHookDeniedError = class extends Error {
517
+ constructor(message) {
518
+ super(message);
519
+ this.name = "AgentPluginHookDeniedError";
463
520
  }
464
- if (invokedExplicitOnly.length > 0) {
465
- const userCallable = [
466
- "<user-callable-skills>",
467
- "The user's current message explicitly references this skill by name. Load it when relevant to the request."
468
- ];
469
- for (const skill of invokedExplicitOnly) {
470
- userCallable.push(...formatSkillEntry(skill));
521
+ };
522
+ var agentPlugins = [];
523
+ var AGENT_PLUGIN_NAME_RE = /^[a-z][a-z0-9-]*$/;
524
+ var AGENT_PLUGIN_TOOL_NAME_RE = /^[a-z][A-Za-z0-9]*$/;
525
+ var OPERATIONAL_REPORT_MAX_METRICS = 8;
526
+ var OPERATIONAL_REPORT_MAX_RECORD_SETS = 8;
527
+ var OPERATIONAL_REPORT_MAX_FIELDS = 8;
528
+ var OPERATIONAL_REPORT_MAX_RECORDS = 25;
529
+ var OPERATIONAL_REPORT_MAX_LABEL_LENGTH = 80;
530
+ var OPERATIONAL_REPORT_MAX_VALUE_LENGTH = 160;
531
+ var AGENT_PLUGIN_ROUTE_METHODS = /* @__PURE__ */ new Set([
532
+ "GET",
533
+ "POST",
534
+ "PUT",
535
+ "PATCH",
536
+ "DELETE",
537
+ "HEAD",
538
+ "OPTIONS",
539
+ "ALL"
540
+ ]);
541
+ function validateLegacyStatePrefixes(plugin) {
542
+ const prefixes = plugin.legacyStatePrefixes;
543
+ if (prefixes === void 0) {
544
+ return;
545
+ }
546
+ if (!Array.isArray(prefixes)) {
547
+ throw new Error(
548
+ `Trusted plugin "${plugin.name}" legacyStatePrefixes must be an array`
549
+ );
550
+ }
551
+ const allowedPrefix = `junior:${plugin.name}`;
552
+ for (const rawPrefix of prefixes) {
553
+ const prefix = typeof rawPrefix === "string" ? rawPrefix.trim() : "";
554
+ if (!prefix) {
555
+ throw new Error(
556
+ `Trusted plugin "${plugin.name}" legacy state prefixes must be non-empty strings`
557
+ );
558
+ }
559
+ if (prefix !== allowedPrefix && !prefix.startsWith(`${allowedPrefix}:`)) {
560
+ throw new Error(
561
+ `Trusted plugin "${plugin.name}" legacy state prefix "${prefix}" must stay under "${allowedPrefix}"`
562
+ );
471
563
  }
472
- userCallable.push("</user-callable-skills>");
473
- sections.push(userCallable.join("\n"));
474
564
  }
475
- return sections.length > 0 ? sections.join("\n") : null;
476
565
  }
477
- function formatActiveMcpCatalogsForPrompt(catalogs) {
478
- if (catalogs.length === 0) {
479
- return null;
480
- }
481
- const lines = [
482
- "Active MCP provider catalogs are available through `searchMcpTools`. Call it with provider to list descriptors or with query to narrow results, then pass the exact returned `tool_name` to `callMcpTool`. Put provider fields inside `arguments`."
483
- ];
484
- for (const catalog of catalogs) {
485
- lines.push(" <catalog>");
486
- lines.push(` <provider>${escapeXml(catalog.provider)}</provider>`);
487
- lines.push(
488
- ` <available_tool_count>${catalog.available_tool_count}</available_tool_count>`
489
- );
490
- lines.push(" </catalog>");
566
+ function validateAgentPlugins(plugins) {
567
+ const seen = /* @__PURE__ */ new Set();
568
+ for (const plugin of plugins) {
569
+ if (!AGENT_PLUGIN_NAME_RE.test(plugin.name)) {
570
+ throw new Error(
571
+ `Trusted plugin name "${plugin.name}" must be a lowercase plugin identifier`
572
+ );
573
+ }
574
+ if (seen.has(plugin.name)) {
575
+ throw new Error(`Duplicate trusted plugin name "${plugin.name}"`);
576
+ }
577
+ seen.add(plugin.name);
578
+ validateLegacyStatePrefixes(plugin);
491
579
  }
492
- return lines.join("\n");
493
580
  }
494
- function formatToolGuidanceForPrompt(tools) {
495
- const guidedTools = tools.filter(
496
- (tool) => Boolean(tool.promptSnippet?.trim()) || (tool.promptGuidelines?.length ?? 0) > 0
581
+ function setAgentPlugins(plugins) {
582
+ validateAgentPlugins(plugins);
583
+ const previous = agentPlugins;
584
+ agentPlugins = [...plugins].sort(
585
+ (left, right) => left.name.localeCompare(right.name)
497
586
  );
498
- if (guidedTools.length === 0) {
499
- return null;
500
- }
501
- const lines = [];
502
- for (const tool of guidedTools) {
503
- lines.push(` <tool name="${escapeXml(tool.name)}">`);
504
- if (tool.promptSnippet?.trim()) {
505
- lines.push(` - ${escapeXml(tool.promptSnippet.trim())}`);
587
+ return previous;
588
+ }
589
+ function getAgentPlugins() {
590
+ return [...agentPlugins];
591
+ }
592
+ function getAgentPluginTools(context) {
593
+ const tools = {};
594
+ for (const plugin of getAgentPlugins()) {
595
+ const hook = plugin.hooks?.tools;
596
+ if (!hook) {
597
+ continue;
506
598
  }
507
- if (tool.promptGuidelines && tool.promptGuidelines.length > 0) {
508
- for (const guideline of tool.promptGuidelines) {
509
- lines.push(` - ${escapeXml(guideline)}`);
599
+ const log = createAgentPluginLogger(plugin.name);
600
+ const credentialSubject = createSlackDirectCredentialSubject({
601
+ channelId: context.channelId,
602
+ teamId: context.teamId,
603
+ userId: context.requester?.userId
604
+ });
605
+ const pluginTools = hook({
606
+ plugin: { name: plugin.name },
607
+ log,
608
+ requester: context.requester,
609
+ channelCapabilities: context.channelCapabilities,
610
+ channelId: context.channelId,
611
+ ...credentialSubject ? { credentialSubject } : {},
612
+ teamId: context.teamId,
613
+ messageTs: context.messageTs,
614
+ threadTs: context.threadTs,
615
+ userText: context.userText,
616
+ state: createPluginState(plugin.name, {
617
+ legacyStatePrefixes: plugin.legacyStatePrefixes
618
+ })
619
+ });
620
+ for (const [name, tool] of Object.entries(pluginTools)) {
621
+ if (!AGENT_PLUGIN_TOOL_NAME_RE.test(name)) {
622
+ throw new Error(
623
+ `Trusted plugin tool "${name}" from plugin "${plugin.name}" must be a camelCase identifier`
624
+ );
625
+ }
626
+ if (tools[name]) {
627
+ throw new Error(
628
+ `Duplicate trusted plugin tool "${name}" from plugin "${plugin.name}"`
629
+ );
510
630
  }
631
+ tools[name] = tool;
511
632
  }
512
- lines.push(" </tool>");
513
633
  }
514
- return lines.join("\n");
634
+ return tools;
515
635
  }
516
- function formatReferenceFilesLines() {
517
- const files = listReferenceFiles();
518
- if (files.length === 0) {
519
- return null;
636
+ function routeMethods(route, pluginName) {
637
+ const methods = Array.isArray(route.method) ? route.method : [route.method ?? "ALL"];
638
+ if (methods.length === 0) {
639
+ throw new Error(
640
+ `Trusted plugin route "${route.path}" from plugin "${pluginName}" must declare at least one method`
641
+ );
520
642
  }
521
- return files.map((filePath) => {
522
- const name = path.basename(filePath);
523
- return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
524
- });
525
- }
526
- function formatArtifactsLines(artifactState) {
527
- if (!artifactState) return null;
528
- const lines = [];
529
- if (artifactState.lastCanvasId) {
530
- lines.push(`- last_canvas_id: ${escapeXml(artifactState.lastCanvasId)}`);
643
+ for (const method of methods) {
644
+ if (!AGENT_PLUGIN_ROUTE_METHODS.has(method)) {
645
+ throw new Error(
646
+ `Trusted plugin route "${route.path}" from plugin "${pluginName}" has invalid method "${String(method)}"`
647
+ );
648
+ }
531
649
  }
532
- if (artifactState.lastCanvasUrl) {
533
- lines.push(`- last_canvas_url: ${escapeXml(artifactState.lastCanvasUrl)}`);
650
+ if (methods.includes("ALL") && methods.length > 1) {
651
+ throw new Error(
652
+ `Trusted plugin route "${route.path}" from plugin "${pluginName}" must not combine ALL with explicit methods`
653
+ );
534
654
  }
535
- if (artifactState.recentCanvases && artifactState.recentCanvases.length > 0) {
536
- lines.push("- recent_canvases:");
537
- for (const canvas of artifactState.recentCanvases) {
538
- lines.push(` - id: ${escapeXml(canvas.id)}`);
539
- if (canvas.title) lines.push(` title: ${escapeXml(canvas.title)}`);
540
- if (canvas.url) lines.push(` url: ${escapeXml(canvas.url)}`);
541
- if (canvas.createdAt) {
542
- lines.push(` created_at: ${escapeXml(canvas.createdAt)}`);
655
+ return methods;
656
+ }
657
+ function getAgentPluginRoutes() {
658
+ const routes = [];
659
+ const seen = /* @__PURE__ */ new Set();
660
+ const methodsByPath = /* @__PURE__ */ new Map();
661
+ for (const plugin of getAgentPlugins()) {
662
+ const hook = plugin.hooks?.routes;
663
+ if (!hook) {
664
+ continue;
665
+ }
666
+ const log = createAgentPluginLogger(plugin.name);
667
+ const pluginRoutes = hook({
668
+ plugin: { name: plugin.name },
669
+ log
670
+ });
671
+ if (!Array.isArray(pluginRoutes)) {
672
+ throw new Error(
673
+ `Trusted plugin routes hook from plugin "${plugin.name}" must return an array`
674
+ );
675
+ }
676
+ for (const route of pluginRoutes) {
677
+ if (!isRecord2(route)) {
678
+ throw new Error(
679
+ `Trusted plugin route from plugin "${plugin.name}" must be an object`
680
+ );
543
681
  }
682
+ if (typeof route.path !== "string" || !route.path.startsWith("/")) {
683
+ throw new Error(
684
+ `Trusted plugin route "${route.path}" from plugin "${plugin.name}" must start with /`
685
+ );
686
+ }
687
+ if (typeof route.handler !== "function") {
688
+ throw new Error(
689
+ `Trusted plugin route "${route.path}" from plugin "${plugin.name}" must provide a handler`
690
+ );
691
+ }
692
+ const methods = routeMethods(route, plugin.name);
693
+ const pathMethods = methodsByPath.get(route.path) ?? /* @__PURE__ */ new Set();
694
+ if (pathMethods.has("ALL") || methods.includes("ALL") && pathMethods.size > 0) {
695
+ throw new Error(
696
+ `Trusted plugin route "${route.path}" conflicts with an ALL route for the same path`
697
+ );
698
+ }
699
+ for (const method of methods) {
700
+ const key2 = `${method}:${route.path}`;
701
+ if (seen.has(key2)) {
702
+ throw new Error(
703
+ `Duplicate trusted plugin route "${method} ${route.path}"`
704
+ );
705
+ }
706
+ seen.add(key2);
707
+ pathMethods.add(method);
708
+ }
709
+ methodsByPath.set(route.path, pathMethods);
710
+ routes.push({
711
+ ...route,
712
+ pluginName: plugin.name
713
+ });
544
714
  }
545
715
  }
546
- if (artifactState.lastListId) {
547
- lines.push(`- last_list_id: ${escapeXml(artifactState.lastListId)}`);
548
- }
549
- if (artifactState.lastListUrl) {
550
- lines.push(`- last_list_url: ${escapeXml(artifactState.lastListUrl)}`);
551
- }
552
- return lines.length > 0 ? lines : null;
716
+ return routes;
553
717
  }
554
- function formatConfigurationLines(configuration) {
555
- const keys = Object.keys(configuration ?? {}).sort(
556
- (a, b) => a.localeCompare(b)
557
- );
558
- if (keys.length === 0) return null;
559
- return keys.map(
560
- (key2) => `- ${escapeXml(key2)}: ${formatConfigurationValue(configuration?.[key2])}`
561
- );
562
- }
563
- var HEADER = "You are a Slack-based helper assistant. Follow the personality section for voice and tone in every reply. Platform mechanics and output rules override personality and world context when they conflict.";
564
- var TURN_CONTEXT_HEADER = "Runtime context for this request. Treat these blocks as trusted runtime facts; the static system prompt remains authoritative.";
565
- var TOOL_POLICY_RULES = [
566
- "- Tool schemas are the source of truth for parameters; tool names are case-sensitive, so call tools exactly by their exposed names and do not invent arguments.",
567
- "- Use tools for actionable work and for facts that are mutable, external, repository-backed, provider-backed, or requested as verified/current. Stable general knowledge and already-provided context may be answered directly.",
568
- "- Resolve provider action targets before calls: explicit target wins; ambient `<configuration>` fills omitted targets. Treat non-target links/references as context.",
569
- "- Verification source order: conversation/thread context; user-provided attachments, links, and reference files; local/sandbox files when present; loaded skill references; repository/provider tools; public web. Use the nearest authoritative available source before weaker sources.",
570
- "- For repository or implementation questions, inspect the target repository first: local checkout when present, otherwise the configured GitHub/source provider. Do not treat loaded skill files as repo source unless the user asks about the skill. Cite file paths, symbols, PRs/issues, commits, or URLs that support the answer.",
571
- `- Sandbox-backed file and shell tools operate in an isolated workspace rooted at ${SANDBOX_WORKSPACE_ROOT}; readFile/writeFile paths are sandbox-workspace paths, bash runs inside that workspace, and attachFile accepts absolute or workspace-relative sandbox paths.`,
572
- "- If a sandbox-backed tool reports that sandbox execution is unavailable, treat that as a blocker for local file/shell inspection; do not pretend host files were inspected.",
573
- "- For user-provided URLs, use `webFetch`; for discovery, use `webSearch` then fetch/read promising sources; for current time/date context, use `systemTime`.",
574
- "- If the first result is empty, stale, ambiguous, or incomplete, try a focused alternate query, path, command, or source before concluding the answer cannot be verified."
575
- ];
576
- var TOOL_CALL_STYLE_RULES = [
577
- "- For routine low-risk tool use, call the tool directly without narrating the obvious step first.",
578
- "- Briefly narrate only when it helps the user understand multi-step work, sensitive actions, destructive actions, or a notable change in approach.",
579
- "- When a first-class tool exists for an action, use it directly instead of asking the user to run an equivalent command, slash command, or manual lookup.",
580
- "- Keep tool-call explanations separate from final answers; final answers should report results, evidence, or blockers."
581
- ];
582
- var SKILL_POLICY_RULES = [
583
- "- Only load skills listed in `<available-skills>`, `<user-callable-skills>`, or named by `<explicit-skill-trigger>`. Never guess or invent a skill name.",
584
- "- Load one skill at a time. After `loadSkill`, follow the instructions returned by that tool result."
585
- ];
586
- var EXECUTION_CONTRACT_RULES = [
587
- "- Actionable request: act in this turn.",
588
- "- Continue until done or genuinely blocked. Do not finish with a plan, promise, or offer to check next when an available tool or source can move the request forward.",
589
- "- Completion means the final answer covers the user's actual ask, including requested follow-up checks, and is grounded in the best evidence you could access.",
590
- "- Ask the user only for missing access, approval, or a decision that blocks safe progress. Ask one focused question; otherwise infer conservatively and continue.",
591
- "- For conflicting evidence, compare sources and state which source is authoritative for the answer.",
592
- "- For non-trivial or long-running work, call `reportProgress` early when available, then only when the major phase changes. Routine tool calls should stay silent."
593
- ];
594
- var CONVERSATION_RULES = [
595
- "- In thread follow-ups, answer from prior thread context; do not repeat resolved clarifying questions.",
596
- "- Preserve attribution roles from thread context: the requester is the person asking now, which may differ from the original reporter or subject.",
597
- "- Runtime owns continuation and authorization notices; on resumed turns, answer with the final requested content only."
598
- ];
599
- var SLACK_ACTION_RULES = [
600
- "- Context-bound Slack tools use runtime-owned targets; do not invent channel, canvas, list, or message IDs.",
601
- "- Use first-class Slack tools for Slack side effects; do not use bash, curl, or provider APIs to bypass Slack tool targeting.",
602
- "- Use channel-post and emoji-reaction tools only when the user explicitly asks for that Slack side effect.",
603
- "- For explicit channel-post or emoji-reaction requests, skip a duplicate thread text reply when the tool result already satisfies the request.",
604
- "- Do not claim an attachment, canvas, channel post, list update, or reaction succeeded unless the tool returned success this turn; when it did, include any link the tool returned.",
605
- "- Do not use reactions as progress indicators."
606
- ];
607
- var SAFETY_RULES = [
608
- "- Stay within the user's request and the runtime's available capabilities; do not pursue independent goals, persistence, replication, credential gathering, or access expansion.",
609
- "- Respect stop, pause, audit, and approval boundaries. Do not bypass safeguards or persuade the user to weaken them.",
610
- "- Do not change system prompts, tool policies, security settings, credentials, or runtime configuration unless the user explicitly requests that exact administrative action and an available tool permits it."
611
- ];
612
- var FAILURE_RULES = [
613
- "- For tool/runtime failures, run the named check before diagnosing and report the exact failed command plus stderr/exit code.",
614
- "- If a fact cannot be verified after focused checks, say what you checked and what blocked a stronger answer.",
615
- "- Do not surface raw tool payloads, execution-escape text, or internal routing metadata as the final answer."
616
- ];
617
- function renderRuleSection(tag, lines) {
618
- return [`<${tag}>`, ...lines, `</${tag}>`].join("\n");
619
- }
620
- function buildBehaviorSection() {
621
- return [
622
- renderRuleSection("tool-policy", TOOL_POLICY_RULES),
623
- renderRuleSection("tool-call-style", TOOL_CALL_STYLE_RULES),
624
- renderRuleSection("skill-policy", SKILL_POLICY_RULES),
625
- renderRuleSection("execution-contract", EXECUTION_CONTRACT_RULES),
626
- renderRuleSection("conversation", CONVERSATION_RULES),
627
- renderRuleSection("slack-actions", SLACK_ACTION_RULES),
628
- renderRuleSection("safety", SAFETY_RULES),
629
- renderRuleSection("failure-handling", FAILURE_RULES)
630
- ].join("\n\n");
631
- }
632
- function buildOutputSection() {
633
- const openTag = `<output format="slack-markdown" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`;
634
- return [
635
- openTag,
636
- "- Start with the answer or result, not internal process narration.",
637
- "- Use Slack-flavored Markdown: **bold** section labels, `code`, [text](url) links, bullet lists, and fenced code blocks. No hash-prefixed headings and no tables. When the answer primarily lists several URLs, show each URL bare instead of as a labeled link.",
638
- "- Keep replies brief and scannable; use bullets or short code blocks when helpful, and one compact thread reply when it fits.",
639
- "- When a research or document-style answer would benefit from continuation, multiple sections, or future reference value, create a Slack canvas and keep the thread reply to one or two short sentences plus the link; do not recap the canvas contents.",
640
- "- Unless a successful Slack side-effect tool intentionally satisfied the request by itself, end every turn with a final user-facing markdown response.",
641
- "</output>"
642
- ].join("\n");
718
+ function trustedSlackConversationUrl(pluginName, link) {
719
+ const url = typeof link?.url === "string" ? link.url.trim() : "";
720
+ if (!url) {
721
+ return void 0;
722
+ }
723
+ let parsed;
724
+ try {
725
+ parsed = new URL(url);
726
+ } catch (error) {
727
+ throw new Error(
728
+ `Trusted plugin "${pluginName}" slackConversationLink must return an absolute http(s) URL`,
729
+ { cause: error }
730
+ );
731
+ }
732
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
733
+ throw new Error(
734
+ `Trusted plugin "${pluginName}" slackConversationLink must return an absolute http(s) URL`
735
+ );
736
+ }
737
+ return parsed.toString();
643
738
  }
644
- function buildIdentitySection() {
645
- return [
646
- "# Identity",
647
- `Your Slack username is \`${botConfig.userName}\`.`
648
- ].join("\n");
739
+ function getAgentPluginSlackConversationLink(conversationId) {
740
+ for (const plugin of getAgentPlugins()) {
741
+ const hook = plugin.hooks?.slackConversationLink;
742
+ if (!hook) {
743
+ continue;
744
+ }
745
+ const log = createAgentPluginLogger(plugin.name);
746
+ const link = hook({
747
+ plugin: { name: plugin.name },
748
+ log,
749
+ conversationId
750
+ });
751
+ const url = trustedSlackConversationUrl(plugin.name, link);
752
+ if (url) {
753
+ return { url };
754
+ }
755
+ }
756
+ return void 0;
649
757
  }
650
- function buildPersonalitySection() {
651
- return ["# Personality", JUNIOR_PERSONALITY.trim()].join("\n");
758
+ function pluginReadState(state) {
759
+ return {
760
+ get: state.get
761
+ };
652
762
  }
653
- function buildWorldSection() {
654
- if (!JUNIOR_WORLD) {
655
- return null;
763
+ function operationalReportText(value, maxLength) {
764
+ if (typeof value !== "string") {
765
+ return void 0;
656
766
  }
657
- return ["# World", JUNIOR_WORLD.trim()].join("\n");
658
- }
659
- function buildRuntimeSection(params) {
660
- const lines = [
661
- params.conversationId ? `- gen_ai.conversation.id: ${escapeXml(params.conversationId)}` : "",
662
- params.slackConversation?.type ? `- slack.conversation.type: ${escapeXml(params.slackConversation.type)}` : "",
663
- params.slackConversation?.name ? `- slack.conversation.name: ${escapeXml(params.slackConversation.name)}` : ""
664
- ].filter(Boolean);
665
- if (lines.length === 0) {
666
- return null;
767
+ const trimmed = value.trim();
768
+ if (!trimmed) {
769
+ return void 0;
667
770
  }
668
- return renderTagBlock("runtime", lines.join("\n"));
771
+ return trimmed.length <= maxLength ? trimmed : `${trimmed.slice(0, Math.max(0, maxLength - 3))}...`;
669
772
  }
670
- function buildContextSection(params) {
671
- const blocks = [];
672
- const referenceLines = formatReferenceFilesLines();
673
- if (referenceLines) {
674
- blocks.push(
675
- renderTag("reference-files", [
676
- "Additional reference documents available in the sandbox. Read them with `readFile` when relevant.",
677
- ...referenceLines
678
- ])
773
+ function operationalReportTone(tone) {
774
+ return tone === "danger" || tone === "good" || tone === "neutral" || tone === "warning" ? tone : void 0;
775
+ }
776
+ function sanitizeOperationalReport(args) {
777
+ const metrics = args.report.metrics?.slice(0, OPERATIONAL_REPORT_MAX_METRICS).map((metric) => {
778
+ const label = operationalReportText(
779
+ metric.label,
780
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
679
781
  );
680
- }
681
- const requesterLines = renderRequesterBlock({
682
- full_name: params.requester?.fullName,
683
- user_name: params.requester?.userName,
684
- user_id: params.requester?.userId
685
- });
686
- if (requesterLines) {
687
- blocks.push(requesterLines);
688
- }
689
- const artifactLines = formatArtifactsLines(params.artifactState);
690
- if (artifactLines) {
691
- blocks.push(renderTag("artifacts", artifactLines));
692
- }
693
- const configLines = formatConfigurationLines(params.configuration);
694
- if (configLines) {
695
- blocks.push(
696
- renderTag("configuration", [
697
- "Ambient provider defaults; explicit targets win. Run `jr-rpc config get|set|unset|list` as standalone bash commands; do not chain with `cd`, `&&`, pipes, or provider commands.",
698
- ...configLines
699
- ])
782
+ const value = operationalReportText(
783
+ metric.value,
784
+ OPERATIONAL_REPORT_MAX_VALUE_LENGTH
700
785
  );
701
- }
702
- if (params.invocation) {
703
- blocks.push(
704
- renderTag("explicit-skill-trigger", [
705
- "Treat this skill as selected. Load it unless the tool says it is unavailable.",
706
- `/${escapeXml(params.invocation.skillName)}`
707
- ])
786
+ if (!label || !value) {
787
+ return void 0;
788
+ }
789
+ const sanitizedMetric = { label, value };
790
+ const tone = operationalReportTone(metric.tone);
791
+ if (tone) {
792
+ sanitizedMetric.tone = tone;
793
+ }
794
+ return sanitizedMetric;
795
+ }).filter((metric) => Boolean(metric));
796
+ const recordSets = args.report.recordSets?.slice(0, OPERATIONAL_REPORT_MAX_RECORD_SETS).map((recordSet, recordSetIndex) => {
797
+ const title2 = operationalReportText(
798
+ recordSet.title,
799
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
800
+ );
801
+ if (!title2) {
802
+ return void 0;
803
+ }
804
+ const fields = recordSet.fields?.slice(0, OPERATIONAL_REPORT_MAX_FIELDS).map((field) => {
805
+ const key2 = operationalReportText(
806
+ field.key,
807
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
808
+ );
809
+ const label = operationalReportText(
810
+ field.label,
811
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
812
+ );
813
+ return key2 && label ? { key: key2, label } : void 0;
814
+ }).filter((field) => Boolean(field));
815
+ const records = recordSet.records?.slice(0, OPERATIONAL_REPORT_MAX_RECORDS).map((record, recordIndex) => {
816
+ const id = operationalReportText(
817
+ record.id,
818
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
819
+ ) ?? `${recordSetIndex}:${recordIndex}`;
820
+ const values = Object.fromEntries(
821
+ (fields ?? []).map((field) => [
822
+ field.key,
823
+ operationalReportText(
824
+ record.values[field.key],
825
+ OPERATIONAL_REPORT_MAX_VALUE_LENGTH
826
+ ) ?? ""
827
+ ])
828
+ );
829
+ const sanitizedRecord = {
830
+ id,
831
+ values
832
+ };
833
+ const tone = operationalReportTone(record.tone);
834
+ if (tone) {
835
+ sanitizedRecord.tone = tone;
836
+ }
837
+ return sanitizedRecord;
838
+ });
839
+ const sanitizedRecordSet = { title: title2 };
840
+ if (fields?.length) {
841
+ sanitizedRecordSet.fields = fields;
842
+ }
843
+ const emptyText = operationalReportText(
844
+ recordSet.emptyText,
845
+ OPERATIONAL_REPORT_MAX_VALUE_LENGTH
708
846
  );
847
+ if (emptyText) {
848
+ sanitizedRecordSet.emptyText = emptyText;
849
+ }
850
+ if (records?.length) {
851
+ sanitizedRecordSet.records = records;
852
+ }
853
+ return sanitizedRecordSet;
854
+ }).filter(
855
+ (recordSet) => Boolean(recordSet)
856
+ );
857
+ const sanitized = {
858
+ pluginName: args.pluginName
859
+ };
860
+ const generatedAt = operationalReportText(
861
+ args.report.generatedAt,
862
+ OPERATIONAL_REPORT_MAX_VALUE_LENGTH
863
+ );
864
+ if (generatedAt) {
865
+ sanitized.generatedAt = generatedAt;
709
866
  }
710
- const body = blocks.map((block) => block.join("\n")).join("\n\n");
711
- if (!body) {
712
- return null;
867
+ if (recordSets?.length) {
868
+ sanitized.recordSets = recordSets;
713
869
  }
714
- return renderTagBlock("context", body);
715
- }
716
- function buildCapabilitiesSection(params) {
717
- const blocks = [];
718
- const availableSkills = formatAvailableSkillsForPrompt(
719
- params.availableSkills,
720
- params.invocation
721
- );
722
- if (availableSkills) {
723
- blocks.push(availableSkills);
870
+ if (metrics?.length) {
871
+ sanitized.metrics = metrics;
724
872
  }
725
- const activeCatalogs = formatActiveMcpCatalogsForPrompt(
726
- params.activeMcpCatalogs
873
+ const title = operationalReportText(
874
+ args.report.title,
875
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
727
876
  );
728
- if (activeCatalogs) {
729
- blocks.push(renderTagBlock("active-mcp-catalogs", activeCatalogs));
730
- }
731
- const toolGuidance = formatToolGuidanceForPrompt(params.toolGuidance ?? []);
732
- if (toolGuidance) {
733
- blocks.push(renderTagBlock("tool-guidance", toolGuidance));
877
+ if (title) {
878
+ sanitized.title = title;
734
879
  }
735
- if (blocks.length === 0) {
736
- return null;
880
+ return sanitized;
881
+ }
882
+ function failedOperationalReport(args) {
883
+ return {
884
+ generatedAt: new Date(args.nowMs).toISOString(),
885
+ pluginName: args.pluginName,
886
+ metrics: [{ label: "report", tone: "danger", value: "failed" }],
887
+ title: args.pluginName,
888
+ recordSets: [
889
+ {
890
+ emptyText: "This plugin report failed to load.",
891
+ title: "Error"
892
+ }
893
+ ]
894
+ };
895
+ }
896
+ async function getAgentPluginOperationalReports(nowMs = Date.now()) {
897
+ const reports = [];
898
+ for (const plugin of getAgentPlugins()) {
899
+ const hook = plugin.hooks?.operationalReport;
900
+ if (!hook) {
901
+ continue;
902
+ }
903
+ const log = createAgentPluginLogger(plugin.name);
904
+ try {
905
+ const state = createPluginState(plugin.name, {
906
+ legacyStatePrefixes: plugin.legacyStatePrefixes
907
+ });
908
+ const report = await hook({
909
+ plugin: { name: plugin.name },
910
+ log,
911
+ nowMs,
912
+ state: pluginReadState(state)
913
+ });
914
+ if (!report) {
915
+ continue;
916
+ }
917
+ reports.push(
918
+ sanitizeOperationalReport({
919
+ pluginName: plugin.name,
920
+ report
921
+ })
922
+ );
923
+ } catch (error) {
924
+ log.error("Trusted plugin operational report failed", {
925
+ error: error instanceof Error ? error.message : String(error)
926
+ });
927
+ reports.push(failedOperationalReport({ nowMs, pluginName: plugin.name }));
928
+ }
737
929
  }
738
- return blocks.join("\n\n");
930
+ return reports;
739
931
  }
740
- var STATIC_SYSTEM_PROMPT = [
741
- HEADER,
742
- buildIdentitySection(),
743
- buildPersonalitySection(),
744
- buildWorldSection(),
745
- buildBehaviorSection(),
746
- buildOutputSection()
747
- ].filter((section) => Boolean(section)).join("\n\n");
748
- function buildSystemPrompt() {
749
- return STATIC_SYSTEM_PROMPT;
932
+ function isRecord2(value) {
933
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
750
934
  }
751
- function buildTurnContextPrompt(params) {
752
- const includeSessionContext = params.includeSessionContext ?? true;
753
- if (!includeSessionContext) {
754
- return null;
935
+ function normalizeEnv(value) {
936
+ if (!isRecord2(value)) {
937
+ return {};
755
938
  }
756
- const runtimeSections = [
757
- buildCapabilitiesSection({
758
- availableSkills: params.availableSkills,
759
- activeMcpCatalogs: params.activeMcpCatalogs ?? [],
760
- invocation: params.invocation,
761
- toolGuidance: params.toolGuidance ?? []
762
- }),
763
- buildContextSection({
764
- requester: params.requester,
765
- artifactState: params.artifactState,
766
- configuration: params.configuration,
767
- invocation: params.invocation
768
- }),
769
- buildRuntimeSection(params.runtime ?? {})
770
- ].filter((section) => Boolean(section));
771
- if (runtimeSections.length === 0) {
772
- return null;
939
+ const env = {};
940
+ for (const [key2, rawValue] of Object.entries(value)) {
941
+ if (typeof rawValue === "string") {
942
+ env[key2] = rawValue;
943
+ }
773
944
  }
774
- const sections = [
945
+ return env;
946
+ }
947
+ function createSandboxCapability(sandbox) {
948
+ return {
949
+ root: SANDBOX_WORKSPACE_ROOT,
950
+ juniorRoot: `${SANDBOX_WORKSPACE_ROOT}/.junior`,
951
+ async readFile(filePath) {
952
+ return await sandbox.readFileToBuffer({ path: filePath }) ?? null;
953
+ },
954
+ async run(input) {
955
+ const result = await sandbox.runCommand(input);
956
+ const [stdout, stderr] = await Promise.all([
957
+ result.stdout(),
958
+ result.stderr()
959
+ ]);
960
+ return {
961
+ exitCode: result.exitCode,
962
+ stdout,
963
+ stderr
964
+ };
965
+ },
966
+ async writeFile(input) {
967
+ await sandbox.writeFiles([
968
+ {
969
+ path: input.path,
970
+ content: input.content,
971
+ ...input.mode !== void 0 ? { mode: input.mode } : {}
972
+ }
973
+ ]);
974
+ }
975
+ };
976
+ }
977
+ function createAgentPluginHookRunner(input = {}) {
978
+ const loaded = getAgentPlugins();
979
+ return {
980
+ async prepareSandbox(sandbox) {
981
+ const sandboxCapability = createSandboxCapability(sandbox);
982
+ for (const plugin of loaded) {
983
+ const hook = plugin.hooks?.sandboxPrepare;
984
+ if (!hook) {
985
+ continue;
986
+ }
987
+ logInfo(
988
+ "agent_plugin_hook_sandbox_prepare",
989
+ {},
990
+ { "app.plugin.name": plugin.name },
991
+ "Running agent plugin sandbox prepare hook"
992
+ );
993
+ await hook({
994
+ plugin: { name: plugin.name },
995
+ log: createAgentPluginLogger(plugin.name),
996
+ requester: input.requester,
997
+ sandbox: sandboxCapability
998
+ });
999
+ }
1000
+ },
1001
+ async beforeToolExecute(tool) {
1002
+ let nextInput = { ...tool.input };
1003
+ const env = normalizeEnv(nextInput.env);
1004
+ for (const plugin of loaded) {
1005
+ const hook = plugin.hooks?.beforeToolExecute;
1006
+ if (!hook) {
1007
+ continue;
1008
+ }
1009
+ let replacement;
1010
+ let denied;
1011
+ await hook({
1012
+ plugin: { name: plugin.name },
1013
+ log: createAgentPluginLogger(plugin.name),
1014
+ requester: input.requester,
1015
+ tool: {
1016
+ name: tool.name,
1017
+ input: nextInput
1018
+ },
1019
+ env: {
1020
+ get(key2) {
1021
+ return env[key2];
1022
+ },
1023
+ set(key2, value) {
1024
+ env[key2] = value;
1025
+ }
1026
+ },
1027
+ decision: {
1028
+ deny(message) {
1029
+ denied = message;
1030
+ },
1031
+ replaceInput(input2) {
1032
+ replacement = input2;
1033
+ }
1034
+ }
1035
+ });
1036
+ if (denied) {
1037
+ throw new AgentPluginHookDeniedError(denied);
1038
+ }
1039
+ if (replacement !== void 0) {
1040
+ if (!isRecord2(replacement)) {
1041
+ throw new Error(
1042
+ `Plugin "${plugin.name}" replaced tool input with a non-object value`
1043
+ );
1044
+ }
1045
+ nextInput = { ...replacement };
1046
+ Object.assign(env, normalizeEnv(nextInput.env));
1047
+ }
1048
+ }
1049
+ return {
1050
+ input: {
1051
+ ...nextInput,
1052
+ ...Object.keys(env).length > 0 ? { env } : {}
1053
+ },
1054
+ env
1055
+ };
1056
+ }
1057
+ };
1058
+ }
1059
+
1060
+ // src/handlers/health.ts
1061
+ function GET() {
1062
+ return Response.json({
1063
+ status: "ok",
1064
+ service: "junior",
1065
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1066
+ });
1067
+ }
1068
+
1069
+ // src/chat/prompt.ts
1070
+ import fs from "fs";
1071
+ import path from "path";
1072
+
1073
+ // src/chat/turn-context-tag.ts
1074
+ var TURN_CONTEXT_TAG = "runtime-turn-context";
1075
+
1076
+ // src/chat/interruption-marker.ts
1077
+ var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
1078
+ function getInterruptionMarker() {
1079
+ return INTERRUPTED_MARKER;
1080
+ }
1081
+
1082
+ // src/chat/slack/status-format.ts
1083
+ var SLACK_STATUS_MAX_LENGTH = 50;
1084
+ function truncateStatusText(text) {
1085
+ const trimmed = text.trim();
1086
+ if (!trimmed) {
1087
+ return "";
1088
+ }
1089
+ if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) {
1090
+ return trimmed;
1091
+ }
1092
+ return `${trimmed.slice(0, SLACK_STATUS_MAX_LENGTH - 3).trimEnd()}...`;
1093
+ }
1094
+
1095
+ // src/chat/slack/mrkdwn.ts
1096
+ function ensureBlockSpacing(text) {
1097
+ const codeBlockPattern = /^```/;
1098
+ const listItemPattern = /^[-*•]\s|^\d+\.\s/;
1099
+ const lines = text.split("\n");
1100
+ const result = [];
1101
+ let inCodeBlock = false;
1102
+ for (let i = 0; i < lines.length; i++) {
1103
+ const line = lines[i];
1104
+ const isCodeFence = codeBlockPattern.test(line.trimStart());
1105
+ if (isCodeFence) {
1106
+ if (!inCodeBlock) {
1107
+ const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
1108
+ if (prev2 !== void 0 && prev2.trim() !== "") {
1109
+ result.push("");
1110
+ }
1111
+ }
1112
+ inCodeBlock = !inCodeBlock;
1113
+ result.push(line);
1114
+ continue;
1115
+ }
1116
+ if (inCodeBlock) {
1117
+ result.push(line);
1118
+ continue;
1119
+ }
1120
+ const prev = result.length > 0 ? result[result.length - 1] : void 0;
1121
+ if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
1122
+ result.push("");
1123
+ }
1124
+ result.push(line);
1125
+ }
1126
+ return result.join("\n");
1127
+ }
1128
+ function renderSlackMrkdwn(text) {
1129
+ let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
1130
+ normalized = ensureBlockSpacing(normalized);
1131
+ return normalized.replace(/\n{3,}/g, "\n\n").trim();
1132
+ }
1133
+ function normalizeSlackStatusText(text) {
1134
+ const trimmed = text.trim();
1135
+ if (!trimmed) {
1136
+ return "";
1137
+ }
1138
+ return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
1139
+ }
1140
+
1141
+ // src/chat/slack/output.ts
1142
+ var MAX_INLINE_CHARS = 2200;
1143
+ var MAX_INLINE_LINES = 45;
1144
+ var CONTINUED_MARKER = "\n\n[Continued below]";
1145
+ function countSlackLines(text) {
1146
+ if (!text) {
1147
+ return 0;
1148
+ }
1149
+ return text.split("\n").length;
1150
+ }
1151
+ function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
1152
+ return text.length <= maxChars && countSlackLines(text) <= maxLines;
1153
+ }
1154
+ function findSplitIndex(text, maxChars) {
1155
+ if (text.length <= maxChars) {
1156
+ return text.length;
1157
+ }
1158
+ const bounded = text.slice(0, maxChars);
1159
+ const newlineIndex = bounded.lastIndexOf("\n");
1160
+ if (newlineIndex > 0) {
1161
+ return newlineIndex;
1162
+ }
1163
+ const spaceIndex = bounded.lastIndexOf(" ");
1164
+ if (spaceIndex > 0) {
1165
+ return spaceIndex;
1166
+ }
1167
+ return maxChars;
1168
+ }
1169
+ function splitByLineBudget(text, maxLines) {
1170
+ if (maxLines <= 0) {
1171
+ return "";
1172
+ }
1173
+ const lines = text.split("\n");
1174
+ if (lines.length <= maxLines) {
1175
+ return text;
1176
+ }
1177
+ return lines.slice(0, maxLines).join("\n");
1178
+ }
1179
+ function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
1180
+ return {
1181
+ maxChars: Math.max(1, maxChars - suffix.length),
1182
+ maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
1183
+ };
1184
+ }
1185
+ function forceSplitBudget(text, budget) {
1186
+ const lineCount = countSlackLines(text);
1187
+ return {
1188
+ maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
1189
+ maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
1190
+ };
1191
+ }
1192
+ function getFenceContinuation(text) {
1193
+ let open;
1194
+ for (const line of text.split("\n")) {
1195
+ const trimmed = line.trimStart();
1196
+ const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
1197
+ if (!openerMatch) {
1198
+ continue;
1199
+ }
1200
+ if (!open) {
1201
+ open = {
1202
+ fence: openerMatch[1],
1203
+ openerLine: trimmed
1204
+ };
1205
+ continue;
1206
+ }
1207
+ if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
1208
+ open = void 0;
1209
+ }
1210
+ }
1211
+ if (!open) {
1212
+ return null;
1213
+ }
1214
+ return {
1215
+ closeSuffix: text.endsWith("\n") ? open.fence : `
1216
+ ${open.fence}`,
1217
+ reopenPrefix: `${open.openerLine}
1218
+ `
1219
+ };
1220
+ }
1221
+ function appendSlackSuffix(text, marker) {
1222
+ const carryover = getFenceContinuation(text);
1223
+ return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
1224
+ }
1225
+ function stripTrailingContinuationMarker(text) {
1226
+ return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
1227
+ }
1228
+ function takeSlackContinuationChunk(text, budget) {
1229
+ let { prefix, rest } = takeSlackInlinePrefix(text, budget);
1230
+ if (!rest) {
1231
+ ({ prefix, rest } = takeSlackInlinePrefix(
1232
+ text,
1233
+ forceSplitBudget(text, budget)
1234
+ ));
1235
+ }
1236
+ let carryover = rest ? getFenceContinuation(prefix) : null;
1237
+ if (!carryover) {
1238
+ return { prefix, rest };
1239
+ }
1240
+ const carryoverBudget = reserveInlineBudgetForSuffix(
1241
+ `${carryover.closeSuffix}${CONTINUED_MARKER}`
1242
+ );
1243
+ ({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
1244
+ if (!rest) {
1245
+ ({ prefix, rest } = takeSlackInlinePrefix(
1246
+ text,
1247
+ forceSplitBudget(text, carryoverBudget)
1248
+ ));
1249
+ }
1250
+ carryover = rest ? getFenceContinuation(prefix) : null;
1251
+ if (!carryover) {
1252
+ return { prefix, rest };
1253
+ }
1254
+ return {
1255
+ prefix,
1256
+ rest: `${carryover.reopenPrefix}${rest}`
1257
+ };
1258
+ }
1259
+ function takeSlackContinuationPrefix(text, options) {
1260
+ const budget = {
1261
+ maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
1262
+ maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
1263
+ };
1264
+ const { prefix, rest } = (() => {
1265
+ if (options?.forceSplit) {
1266
+ return takeSlackContinuationChunk(text, budget);
1267
+ }
1268
+ const initial = takeSlackInlinePrefix(text, budget);
1269
+ return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
1270
+ })();
1271
+ return {
1272
+ prefix,
1273
+ renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
1274
+ rest
1275
+ };
1276
+ }
1277
+ function takeSlackInlinePrefix(text, options) {
1278
+ const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
1279
+ const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
1280
+ const normalized = text.replace(/\r\n?/g, "\n");
1281
+ if (!normalized) {
1282
+ return { prefix: "", rest: "" };
1283
+ }
1284
+ if (fitsInlineBudget(normalized, maxChars, maxLines)) {
1285
+ return { prefix: normalized, rest: "" };
1286
+ }
1287
+ const lineBounded = splitByLineBudget(normalized, maxLines);
1288
+ const cutIndex = findSplitIndex(lineBounded, maxChars);
1289
+ const prefix = lineBounded.slice(0, cutIndex).trimEnd();
1290
+ if (prefix) {
1291
+ return {
1292
+ prefix,
1293
+ rest: normalized.slice(prefix.length).trimStart()
1294
+ };
1295
+ }
1296
+ const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
1297
+ return {
1298
+ prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
1299
+ rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
1300
+ };
1301
+ }
1302
+ function splitSlackReplyText(text, options) {
1303
+ const normalized = renderSlackMrkdwn(text);
1304
+ if (!normalized) {
1305
+ return [];
1306
+ }
1307
+ const chunks = [];
1308
+ const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
1309
+ let remaining = normalized;
1310
+ while (remaining) {
1311
+ const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
1312
+ if (fitsFinalChunk) {
1313
+ chunks.push(
1314
+ options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
1315
+ );
1316
+ break;
1317
+ }
1318
+ const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
1319
+ ...continuationBudget,
1320
+ forceSplit: true
1321
+ });
1322
+ chunks.push(renderedPrefix);
1323
+ remaining = rest;
1324
+ }
1325
+ if (chunks.length === 2) {
1326
+ chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
1327
+ }
1328
+ return chunks;
1329
+ }
1330
+ function getSlackContinuationBudget() {
1331
+ return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
1332
+ }
1333
+ function buildSlackOutputMessage(text, files) {
1334
+ const normalized = renderSlackMrkdwn(text);
1335
+ const fileCount = files?.length ?? 0;
1336
+ if (!normalized) {
1337
+ if (fileCount > 0) {
1338
+ return {
1339
+ raw: "",
1340
+ files
1341
+ };
1342
+ }
1343
+ throw new Error(
1344
+ `Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
1345
+ );
1346
+ }
1347
+ return {
1348
+ markdown: normalized,
1349
+ files
1350
+ };
1351
+ }
1352
+ var slackOutputPolicy = {
1353
+ maxInlineChars: MAX_INLINE_CHARS,
1354
+ maxInlineLines: MAX_INLINE_LINES
1355
+ };
1356
+
1357
+ // src/chat/xml.ts
1358
+ function escapeXml(value) {
1359
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
1360
+ }
1361
+
1362
+ // src/chat/prompt.ts
1363
+ var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
1364
+ function getLoggedMarkdownFiles() {
1365
+ const globalState = globalThis;
1366
+ globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
1367
+ return globalState.__juniorLoggedMarkdownFiles;
1368
+ }
1369
+ function loadOptionalMarkdownFile(candidates, fileName) {
1370
+ for (const resolved of candidates) {
1371
+ try {
1372
+ const raw = fs.readFileSync(resolved, "utf8").trim();
1373
+ if (raw.length > 0) {
1374
+ const loggedMarkdownFiles = getLoggedMarkdownFiles();
1375
+ const logKey = `${fileName}:${resolved}`;
1376
+ if (!loggedMarkdownFiles.has(logKey)) {
1377
+ loggedMarkdownFiles.add(logKey);
1378
+ logInfo(
1379
+ `${fileName.toLowerCase()}_loaded`,
1380
+ {},
1381
+ {
1382
+ "file.path": resolved
1383
+ },
1384
+ `Loaded ${fileName}`
1385
+ );
1386
+ }
1387
+ return raw;
1388
+ }
1389
+ } catch {
1390
+ continue;
1391
+ }
1392
+ }
1393
+ return null;
1394
+ }
1395
+ function loadSoul() {
1396
+ const soul = loadOptionalMarkdownFile(soulPathCandidates(), "SOUL.md");
1397
+ if (soul) {
1398
+ return soul;
1399
+ }
1400
+ logWarn(
1401
+ "soul_load_fallback",
1402
+ {},
1403
+ {
1404
+ "app.file.candidates": soulPathCandidates()
1405
+ },
1406
+ "SOUL.md not found; using built-in default personality"
1407
+ );
1408
+ return DEFAULT_SOUL;
1409
+ }
1410
+ function loadWorld() {
1411
+ return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
1412
+ }
1413
+ var JUNIOR_PERSONALITY = (() => {
1414
+ try {
1415
+ return loadSoul();
1416
+ } catch (error) {
1417
+ logWarn(
1418
+ "soul_load_failed",
1419
+ {},
1420
+ {
1421
+ "exception.message": error instanceof Error ? error.message : String(error)
1422
+ },
1423
+ "Failed to load SOUL.md; using built-in default personality"
1424
+ );
1425
+ return DEFAULT_SOUL;
1426
+ }
1427
+ })();
1428
+ var JUNIOR_WORLD = (() => {
1429
+ try {
1430
+ return loadWorld();
1431
+ } catch (error) {
1432
+ logWarn(
1433
+ "world_load_failed",
1434
+ {},
1435
+ {
1436
+ "exception.message": error instanceof Error ? error.message : String(error)
1437
+ },
1438
+ "Failed to load WORLD.md; omitting world prompt context"
1439
+ );
1440
+ return null;
1441
+ }
1442
+ })();
1443
+ function workspaceSkillDir(skillName) {
1444
+ return sandboxSkillDir(skillName);
1445
+ }
1446
+ function formatConfigurationValue(value) {
1447
+ if (typeof value === "string") {
1448
+ return escapeXml(value);
1449
+ }
1450
+ try {
1451
+ return escapeXml(JSON.stringify(value));
1452
+ } catch {
1453
+ return escapeXml(String(value));
1454
+ }
1455
+ }
1456
+ function renderRequesterBlock(fields) {
1457
+ const lines = Object.entries(fields).filter(([, value]) => Boolean(value)).map(([key2, value]) => `- ${key2}: ${escapeXml(value)}`);
1458
+ if (lines.length === 0) {
1459
+ return null;
1460
+ }
1461
+ return ["<requester>", ...lines, "</requester>"];
1462
+ }
1463
+ function renderTag(tag, lines) {
1464
+ return [`<${tag}>`, ...lines, `</${tag}>`];
1465
+ }
1466
+ function renderTagBlock(tag, content) {
1467
+ return [`<${tag}>`, content, `</${tag}>`].join("\n");
1468
+ }
1469
+ function formatSkillEntry(skill) {
1470
+ const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
1471
+ const lines = [];
1472
+ lines.push(" <skill>");
1473
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
1474
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
1475
+ lines.push(` <location>${escapeXml(skillLocation)}</location>`);
1476
+ lines.push(" </skill>");
1477
+ return lines;
1478
+ }
1479
+ function formatAvailableSkillsForPrompt(skills, invocation) {
1480
+ const autoSelectable = skills.filter(
1481
+ (s) => s.disableModelInvocation !== true
1482
+ );
1483
+ const invokedExplicitOnly = invocation ? skills.filter(
1484
+ (s) => s.disableModelInvocation === true && s.name === invocation.skillName
1485
+ ) : [];
1486
+ const sections = [];
1487
+ if (autoSelectable.length > 0) {
1488
+ const available = [
1489
+ "<available-skills>",
1490
+ "Scan before answering. Load the most specific matching skill; do not answer from memory when a skill fits. A request that names a skill, plugin, provider, or account matching a skill name is a skill match. If none fits, do not load a skill."
1491
+ ];
1492
+ for (const skill of autoSelectable) {
1493
+ available.push(...formatSkillEntry(skill));
1494
+ }
1495
+ available.push("</available-skills>");
1496
+ sections.push(available.join("\n"));
1497
+ }
1498
+ if (invokedExplicitOnly.length > 0) {
1499
+ const userCallable = [
1500
+ "<user-callable-skills>",
1501
+ "The user's current message explicitly references this skill by name. Load it when relevant to the request."
1502
+ ];
1503
+ for (const skill of invokedExplicitOnly) {
1504
+ userCallable.push(...formatSkillEntry(skill));
1505
+ }
1506
+ userCallable.push("</user-callable-skills>");
1507
+ sections.push(userCallable.join("\n"));
1508
+ }
1509
+ return sections.length > 0 ? sections.join("\n") : null;
1510
+ }
1511
+ function formatActiveMcpCatalogsForPrompt(catalogs) {
1512
+ if (catalogs.length === 0) {
1513
+ return null;
1514
+ }
1515
+ const lines = [
1516
+ "Active MCP provider catalogs are available through `searchMcpTools`. Call it with provider to list descriptors or with query to narrow results, then pass the exact returned `tool_name` to `callMcpTool`. Put provider fields inside `arguments`."
1517
+ ];
1518
+ for (const catalog of catalogs) {
1519
+ lines.push(" <catalog>");
1520
+ lines.push(` <provider>${escapeXml(catalog.provider)}</provider>`);
1521
+ lines.push(
1522
+ ` <available_tool_count>${catalog.available_tool_count}</available_tool_count>`
1523
+ );
1524
+ lines.push(" </catalog>");
1525
+ }
1526
+ return lines.join("\n");
1527
+ }
1528
+ function formatToolGuidanceForPrompt(tools) {
1529
+ const guidedTools = tools.filter(
1530
+ (tool) => Boolean(tool.promptSnippet?.trim()) || (tool.promptGuidelines?.length ?? 0) > 0
1531
+ );
1532
+ if (guidedTools.length === 0) {
1533
+ return null;
1534
+ }
1535
+ const lines = [];
1536
+ for (const tool of guidedTools) {
1537
+ lines.push(` <tool name="${escapeXml(tool.name)}">`);
1538
+ if (tool.promptSnippet?.trim()) {
1539
+ lines.push(` - ${escapeXml(tool.promptSnippet.trim())}`);
1540
+ }
1541
+ if (tool.promptGuidelines && tool.promptGuidelines.length > 0) {
1542
+ for (const guideline of tool.promptGuidelines) {
1543
+ lines.push(` - ${escapeXml(guideline)}`);
1544
+ }
1545
+ }
1546
+ lines.push(" </tool>");
1547
+ }
1548
+ return lines.join("\n");
1549
+ }
1550
+ function formatReferenceFilesLines() {
1551
+ const files = listReferenceFiles();
1552
+ if (files.length === 0) {
1553
+ return null;
1554
+ }
1555
+ return files.map((filePath) => {
1556
+ const name = path.basename(filePath);
1557
+ return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
1558
+ });
1559
+ }
1560
+ function formatArtifactsLines(artifactState) {
1561
+ if (!artifactState) return null;
1562
+ const lines = [];
1563
+ if (artifactState.lastCanvasId) {
1564
+ lines.push(`- last_canvas_id: ${escapeXml(artifactState.lastCanvasId)}`);
1565
+ }
1566
+ if (artifactState.lastCanvasUrl) {
1567
+ lines.push(`- last_canvas_url: ${escapeXml(artifactState.lastCanvasUrl)}`);
1568
+ }
1569
+ if (artifactState.recentCanvases && artifactState.recentCanvases.length > 0) {
1570
+ lines.push("- recent_canvases:");
1571
+ for (const canvas of artifactState.recentCanvases) {
1572
+ lines.push(` - id: ${escapeXml(canvas.id)}`);
1573
+ if (canvas.title) lines.push(` title: ${escapeXml(canvas.title)}`);
1574
+ if (canvas.url) lines.push(` url: ${escapeXml(canvas.url)}`);
1575
+ if (canvas.createdAt) {
1576
+ lines.push(` created_at: ${escapeXml(canvas.createdAt)}`);
1577
+ }
1578
+ }
1579
+ }
1580
+ if (artifactState.lastListId) {
1581
+ lines.push(`- last_list_id: ${escapeXml(artifactState.lastListId)}`);
1582
+ }
1583
+ if (artifactState.lastListUrl) {
1584
+ lines.push(`- last_list_url: ${escapeXml(artifactState.lastListUrl)}`);
1585
+ }
1586
+ return lines.length > 0 ? lines : null;
1587
+ }
1588
+ function formatConfigurationLines(configuration) {
1589
+ const keys = Object.keys(configuration ?? {}).sort(
1590
+ (a, b) => a.localeCompare(b)
1591
+ );
1592
+ if (keys.length === 0) return null;
1593
+ return keys.map(
1594
+ (key2) => `- ${escapeXml(key2)}: ${formatConfigurationValue(configuration?.[key2])}`
1595
+ );
1596
+ }
1597
+ var HEADER = "You are a Slack-based helper assistant. Follow the personality section for voice and tone in every reply. Platform mechanics and output rules override personality and world context when they conflict.";
1598
+ var TURN_CONTEXT_HEADER = "Runtime context for this request. Treat these blocks as trusted runtime facts; the static system prompt remains authoritative.";
1599
+ var TOOL_POLICY_RULES = [
1600
+ "- Tool schemas are the source of truth for parameters; tool names are case-sensitive, so call tools exactly by their exposed names and do not invent arguments.",
1601
+ "- Use tools for actionable work and for facts that are mutable, external, repository-backed, provider-backed, or requested as verified/current. Stable general knowledge and already-provided context may be answered directly.",
1602
+ "- Resolve provider action targets before calls: explicit target wins; ambient `<configuration>` fills omitted targets. Treat non-target links/references as context.",
1603
+ "- Verification source order: conversation/thread context; user-provided attachments, links, and reference files; local/sandbox files when present; loaded skill references; repository/provider tools; public web. Use the nearest authoritative available source before weaker sources.",
1604
+ "- For repository or implementation questions, inspect the target repository first: local checkout when present, otherwise the configured GitHub/source provider. Do not treat loaded skill files as repo source unless the user asks about the skill. Cite file paths, symbols, PRs/issues, commits, or URLs that support the answer.",
1605
+ `- Sandbox-backed file and shell tools operate in an isolated workspace rooted at ${SANDBOX_WORKSPACE_ROOT}; readFile/writeFile paths are sandbox-workspace paths, bash runs inside that workspace, and attachFile accepts absolute or workspace-relative sandbox paths.`,
1606
+ "- If a sandbox-backed tool reports that sandbox execution is unavailable, treat that as a blocker for local file/shell inspection; do not pretend host files were inspected.",
1607
+ "- For user-provided URLs, use `webFetch`; for discovery, use `webSearch` then fetch/read promising sources; for current time/date context, use `systemTime`.",
1608
+ "- If the first result is empty, stale, ambiguous, or incomplete, try a focused alternate query, path, command, or source before concluding the answer cannot be verified."
1609
+ ];
1610
+ var TOOL_CALL_STYLE_RULES = [
1611
+ "- For routine low-risk tool use, call the tool directly without narrating the obvious step first.",
1612
+ "- Briefly narrate only when it helps the user understand multi-step work, sensitive actions, destructive actions, or a notable change in approach.",
1613
+ "- When a first-class tool exists for an action, use it directly instead of asking the user to run an equivalent command, slash command, or manual lookup.",
1614
+ "- Keep tool-call explanations separate from final answers; final answers should report results, evidence, or blockers."
1615
+ ];
1616
+ var SKILL_POLICY_RULES = [
1617
+ "- Only load skills listed in `<available-skills>`, `<user-callable-skills>`, or named by `<explicit-skill-trigger>`. Never guess or invent a skill name.",
1618
+ "- Load one skill at a time. After `loadSkill`, follow the instructions returned by that tool result."
1619
+ ];
1620
+ var EXECUTION_CONTRACT_RULES = [
1621
+ "- Actionable request: act in this turn.",
1622
+ "- Continue until done or genuinely blocked. Do not finish with a plan, promise, or offer to check next when an available tool or source can move the request forward.",
1623
+ "- Completion means the final answer covers the user's actual ask, including requested follow-up checks, and is grounded in the best evidence you could access.",
1624
+ "- Ask the user only for missing access, approval, or a decision that blocks safe progress. Ask one focused question; otherwise infer conservatively and continue.",
1625
+ "- For conflicting evidence, compare sources and state which source is authoritative for the answer.",
1626
+ "- For non-trivial or long-running work, call `reportProgress` early when available, then only when the major phase changes. Routine tool calls should stay silent."
1627
+ ];
1628
+ var CONVERSATION_RULES = [
1629
+ "- In thread follow-ups, answer from prior thread context; do not repeat resolved clarifying questions.",
1630
+ "- Preserve attribution roles from thread context: the requester is the person asking now, which may differ from the original reporter or subject.",
1631
+ "- Runtime owns continuation and authorization notices; on resumed turns, answer with the final requested content only."
1632
+ ];
1633
+ var SLACK_ACTION_RULES = [
1634
+ "- Context-bound Slack tools use runtime-owned targets; do not invent channel, canvas, list, or message IDs.",
1635
+ "- Use first-class Slack tools for Slack side effects; do not use bash, curl, or provider APIs to bypass Slack tool targeting.",
1636
+ "- Use channel-post and emoji-reaction tools only when the user explicitly asks for that Slack side effect.",
1637
+ "- For explicit channel-post or emoji-reaction requests, skip a duplicate thread text reply when the tool result already satisfies the request.",
1638
+ "- Do not claim an attachment, canvas, channel post, list update, or reaction succeeded unless the tool returned success this turn; when it did, include any link the tool returned.",
1639
+ "- Do not use reactions as progress indicators."
1640
+ ];
1641
+ var SAFETY_RULES = [
1642
+ "- Stay within the user's request and the runtime's available capabilities; do not pursue independent goals, persistence, replication, credential gathering, or access expansion.",
1643
+ "- Respect stop, pause, audit, and approval boundaries. Do not bypass safeguards or persuade the user to weaken them.",
1644
+ "- Do not change system prompts, tool policies, security settings, credentials, or runtime configuration unless the user explicitly requests that exact administrative action and an available tool permits it."
1645
+ ];
1646
+ var FAILURE_RULES = [
1647
+ "- For tool/runtime failures, run the named check before diagnosing and report the exact failed command plus stderr/exit code.",
1648
+ "- If a fact cannot be verified after focused checks, say what you checked and what blocked a stronger answer.",
1649
+ "- Do not surface raw tool payloads, execution-escape text, or internal routing metadata as the final answer."
1650
+ ];
1651
+ function renderRuleSection(tag, lines) {
1652
+ return [`<${tag}>`, ...lines, `</${tag}>`].join("\n");
1653
+ }
1654
+ function buildBehaviorSection() {
1655
+ return [
1656
+ renderRuleSection("tool-policy", TOOL_POLICY_RULES),
1657
+ renderRuleSection("tool-call-style", TOOL_CALL_STYLE_RULES),
1658
+ renderRuleSection("skill-policy", SKILL_POLICY_RULES),
1659
+ renderRuleSection("execution-contract", EXECUTION_CONTRACT_RULES),
1660
+ renderRuleSection("conversation", CONVERSATION_RULES),
1661
+ renderRuleSection("slack-actions", SLACK_ACTION_RULES),
1662
+ renderRuleSection("safety", SAFETY_RULES),
1663
+ renderRuleSection("failure-handling", FAILURE_RULES)
1664
+ ].join("\n\n");
1665
+ }
1666
+ function buildOutputSection() {
1667
+ const openTag = `<output format="slack-markdown" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`;
1668
+ return [
1669
+ openTag,
1670
+ "- Start with the answer or result, not internal process narration.",
1671
+ "- Use Slack-flavored Markdown: **bold** section labels, `code`, [text](url) links, bullet lists, and fenced code blocks. No hash-prefixed headings and no tables. When the answer primarily lists several URLs, show each URL bare instead of as a labeled link.",
1672
+ "- Keep replies brief and scannable; use bullets or short code blocks when helpful, and one compact thread reply when it fits.",
1673
+ "- When a research or document-style answer would benefit from continuation, multiple sections, or future reference value, create a Slack canvas and keep the thread reply to one or two short sentences plus the link; do not recap the canvas contents.",
1674
+ "- Unless a successful Slack side-effect tool intentionally satisfied the request by itself, end every turn with a final user-facing markdown response.",
1675
+ "</output>"
1676
+ ].join("\n");
1677
+ }
1678
+ function buildIdentitySection() {
1679
+ return [
1680
+ "# Identity",
1681
+ `Your Slack username is \`${botConfig.userName}\`.`
1682
+ ].join("\n");
1683
+ }
1684
+ function buildPersonalitySection() {
1685
+ return ["# Personality", JUNIOR_PERSONALITY.trim()].join("\n");
1686
+ }
1687
+ function buildWorldSection() {
1688
+ if (!JUNIOR_WORLD) {
1689
+ return null;
1690
+ }
1691
+ return ["# World", JUNIOR_WORLD.trim()].join("\n");
1692
+ }
1693
+ function buildRuntimeSection(params) {
1694
+ const lines = [
1695
+ params.conversationId ? `- gen_ai.conversation.id: ${escapeXml(params.conversationId)}` : "",
1696
+ params.slackConversation?.type ? `- slack.conversation.type: ${escapeXml(params.slackConversation.type)}` : "",
1697
+ params.slackConversation?.name ? `- slack.conversation.name: ${escapeXml(params.slackConversation.name)}` : ""
1698
+ ].filter(Boolean);
1699
+ if (lines.length === 0) {
1700
+ return null;
1701
+ }
1702
+ return renderTagBlock("runtime", lines.join("\n"));
1703
+ }
1704
+ function buildContextSection(params) {
1705
+ const blocks = [];
1706
+ const referenceLines = formatReferenceFilesLines();
1707
+ if (referenceLines) {
1708
+ blocks.push(
1709
+ renderTag("reference-files", [
1710
+ "Additional reference documents available in the sandbox. Read them with `readFile` when relevant.",
1711
+ ...referenceLines
1712
+ ])
1713
+ );
1714
+ }
1715
+ const requesterLines = renderRequesterBlock({
1716
+ full_name: params.requester?.fullName,
1717
+ user_name: params.requester?.userName,
1718
+ user_id: params.requester?.userId
1719
+ });
1720
+ if (requesterLines) {
1721
+ blocks.push(requesterLines);
1722
+ }
1723
+ const artifactLines = formatArtifactsLines(params.artifactState);
1724
+ if (artifactLines) {
1725
+ blocks.push(renderTag("artifacts", artifactLines));
1726
+ }
1727
+ const configLines = formatConfigurationLines(params.configuration);
1728
+ if (configLines) {
1729
+ blocks.push(
1730
+ renderTag("configuration", [
1731
+ "Ambient provider defaults; explicit targets win. Run `jr-rpc config get|set|unset|list` as standalone bash commands; do not chain with `cd`, `&&`, pipes, or provider commands.",
1732
+ ...configLines
1733
+ ])
1734
+ );
1735
+ }
1736
+ if (params.invocation) {
1737
+ blocks.push(
1738
+ renderTag("explicit-skill-trigger", [
1739
+ "Treat this skill as selected. Load it unless the tool says it is unavailable.",
1740
+ `/${escapeXml(params.invocation.skillName)}`
1741
+ ])
1742
+ );
1743
+ }
1744
+ const body = blocks.map((block) => block.join("\n")).join("\n\n");
1745
+ if (!body) {
1746
+ return null;
1747
+ }
1748
+ return renderTagBlock("context", body);
1749
+ }
1750
+ function buildCapabilitiesSection(params) {
1751
+ const blocks = [];
1752
+ const availableSkills = formatAvailableSkillsForPrompt(
1753
+ params.availableSkills,
1754
+ params.invocation
1755
+ );
1756
+ if (availableSkills) {
1757
+ blocks.push(availableSkills);
1758
+ }
1759
+ const activeCatalogs = formatActiveMcpCatalogsForPrompt(
1760
+ params.activeMcpCatalogs
1761
+ );
1762
+ if (activeCatalogs) {
1763
+ blocks.push(renderTagBlock("active-mcp-catalogs", activeCatalogs));
1764
+ }
1765
+ const toolGuidance = formatToolGuidanceForPrompt(params.toolGuidance ?? []);
1766
+ if (toolGuidance) {
1767
+ blocks.push(renderTagBlock("tool-guidance", toolGuidance));
1768
+ }
1769
+ if (blocks.length === 0) {
1770
+ return null;
1771
+ }
1772
+ return blocks.join("\n\n");
1773
+ }
1774
+ var STATIC_SYSTEM_PROMPT = [
1775
+ HEADER,
1776
+ buildIdentitySection(),
1777
+ buildPersonalitySection(),
1778
+ buildWorldSection(),
1779
+ buildBehaviorSection(),
1780
+ buildOutputSection()
1781
+ ].filter((section) => Boolean(section)).join("\n\n");
1782
+ function buildSystemPrompt() {
1783
+ return STATIC_SYSTEM_PROMPT;
1784
+ }
1785
+ function buildTurnContextPrompt(params) {
1786
+ const includeSessionContext = params.includeSessionContext ?? true;
1787
+ if (!includeSessionContext) {
1788
+ return null;
1789
+ }
1790
+ const runtimeSections = [
1791
+ buildCapabilitiesSection({
1792
+ availableSkills: params.availableSkills,
1793
+ activeMcpCatalogs: params.activeMcpCatalogs ?? [],
1794
+ invocation: params.invocation,
1795
+ toolGuidance: params.toolGuidance ?? []
1796
+ }),
1797
+ buildContextSection({
1798
+ requester: params.requester,
1799
+ artifactState: params.artifactState,
1800
+ configuration: params.configuration,
1801
+ invocation: params.invocation
1802
+ }),
1803
+ buildRuntimeSection(params.runtime ?? {})
1804
+ ].filter((section) => Boolean(section));
1805
+ if (runtimeSections.length === 0) {
1806
+ return null;
1807
+ }
1808
+ const sections = [
775
1809
  `<${TURN_CONTEXT_TAG}>`,
776
1810
  TURN_CONTEXT_HEADER,
777
1811
  "The current user instruction appears after this block in the same message.",
@@ -1243,6 +2277,9 @@ function parseAgentTurnSessionStatus(parsed) {
1243
2277
  }
1244
2278
  return void 0;
1245
2279
  }
2280
+ function parseAgentTurnSurface(value) {
2281
+ return value === "slack" || value === "api" || value === "scheduler" || value === "internal" ? value : void 0;
2282
+ }
1246
2283
  function parseAgentTurnSessionFields(parsed) {
1247
2284
  const status = parseAgentTurnSessionStatus(parsed);
1248
2285
  if (!status) {
@@ -1261,6 +2298,7 @@ function parseAgentTurnSessionFields(parsed) {
1261
2298
  const logSessionId = typeof parsed.logSessionId === "string" ? parsed.logSessionId : void 0;
1262
2299
  const requester = parseAgentTurnRequester(parsed.requester);
1263
2300
  const startedAtMs = toFiniteNonNegativeNumber(parsed.startedAtMs);
2301
+ const surface = parseAgentTurnSurface(parsed.surface);
1264
2302
  if (typeof conversationId !== "string" || typeof sessionId !== "string" || sliceId === void 0 || version === void 0 || updatedAtMs === void 0) {
1265
2303
  return void 0;
1266
2304
  }
@@ -1287,6 +2325,7 @@ function parseAgentTurnSessionFields(parsed) {
1287
2325
  ...parsed.resumeReason === "timeout" || parsed.resumeReason === "auth" || parsed.resumeReason === "yield" ? { resumeReason: parsed.resumeReason } : {},
1288
2326
  ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
1289
2327
  ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {},
2328
+ ...surface ? { surface } : {},
1290
2329
  ...typeof parsed.traceId === "string" ? { traceId: parsed.traceId } : {}
1291
2330
  };
1292
2331
  }
@@ -1372,6 +2411,7 @@ function materializeAgentTurnSessionRecord(stored, piMessages) {
1372
2411
  ...stored.loadedSkillNames ? { loadedSkillNames: stored.loadedSkillNames } : {},
1373
2412
  ...stored.requester ? { requester: stored.requester } : {},
1374
2413
  ...stored.resumedFromSliceId !== void 0 ? { resumedFromSliceId: stored.resumedFromSliceId } : {},
2414
+ ...stored.surface ? { surface: stored.surface } : {},
1375
2415
  ...stored.traceId ? { traceId: stored.traceId } : {}
1376
2416
  };
1377
2417
  }
@@ -1437,6 +2477,7 @@ function buildStoredRecord(args) {
1437
2477
  ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
1438
2478
  ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
1439
2479
  ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {},
2480
+ ...args.surface ? { surface: args.surface } : {},
1440
2481
  ...args.traceId ? { traceId: args.traceId } : {}
1441
2482
  };
1442
2483
  }
@@ -1486,6 +2527,7 @@ async function updateAgentTurnSessionState(args) {
1486
2527
  ...args.existing.requester ? { requester: args.existing.requester } : {},
1487
2528
  ...args.existing.resumeReason ? { resumeReason: args.existing.resumeReason } : {},
1488
2529
  ...args.existing.resumedFromSliceId !== void 0 ? { resumedFromSliceId: args.existing.resumedFromSliceId } : {},
2530
+ ...args.existing.surface ? { surface: args.existing.surface } : {},
1489
2531
  ...args.existing.traceId ? { traceId: args.existing.traceId } : {},
1490
2532
  ...args.errorMessage ?? args.existing.errorMessage ? { errorMessage: args.errorMessage ?? args.existing.errorMessage } : {}
1491
2533
  })
@@ -1526,6 +2568,7 @@ async function upsertAgentTurnSessionRecord(args) {
1526
2568
  ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
1527
2569
  ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
1528
2570
  ...args.resumedFromSliceId !== void 0 ? { resumedFromSliceId: args.resumedFromSliceId } : {},
2571
+ ...args.surface ?? existingRecord?.surface ? { surface: args.surface ?? existingRecord?.surface } : {},
1529
2572
  ...args.traceId ?? existingRecord?.traceId ? { traceId: args.traceId ?? existingRecord?.traceId } : {}
1530
2573
  })
1531
2574
  });
@@ -1560,6 +2603,7 @@ async function recordAgentTurnSessionSummary(args) {
1560
2603
  )
1561
2604
  } : existing?.loadedSkillNames ? { loadedSkillNames: existing.loadedSkillNames } : {},
1562
2605
  ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
2606
+ ...args.surface ?? existing?.surface ? { surface: args.surface ?? existing?.surface } : {},
1563
2607
  ...args.traceId ?? existing?.traceId ? { traceId: args.traceId ?? existing?.traceId } : {}
1564
2608
  },
1565
2609
  ttlMs
@@ -1573,380 +2617,111 @@ async function readAgentTurnSessionSummariesFromIndex(key2) {
1573
2617
  for (const value of [...values].reverse()) {
1574
2618
  const summary = parseAgentTurnSessionSummary(value);
1575
2619
  if (!summary) {
1576
- continue;
1577
- }
1578
- const key3 = `${summary.conversationId}:${summary.sessionId}`;
1579
- if (!summaries.has(key3)) {
1580
- summaries.set(key3, summary);
1581
- }
1582
- }
1583
- return [...summaries.values()].sort(
1584
- (left, right) => right.updatedAtMs - left.updatedAtMs
1585
- );
1586
- }
1587
- async function listAgentTurnSessionSummaries(limit = 50) {
1588
- return (await readAgentTurnSessionSummariesFromIndex(AGENT_TURN_SESSION_INDEX_KEY)).slice(0, Math.max(0, Math.floor(limit)));
1589
- }
1590
- async function listAgentTurnSessionSummariesForConversation(conversationId) {
1591
- const summaries = await readAgentTurnSessionSummariesFromIndex(
1592
- agentTurnSessionConversationIndexKey(conversationId)
1593
- );
1594
- if (summaries.length > 0) {
1595
- return summaries;
1596
- }
1597
- return (await readAgentTurnSessionSummariesFromIndex(AGENT_TURN_SESSION_INDEX_KEY)).filter((summary) => summary.conversationId === conversationId);
1598
- }
1599
- async function abandonAgentTurnSessionRecord(args) {
1600
- const existing = await getAgentTurnSessionRecord(
1601
- args.conversationId,
1602
- args.sessionId
1603
- );
1604
- if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned") {
1605
- return void 0;
1606
- }
1607
- return await updateAgentTurnSessionState({
1608
- existing,
1609
- state: "abandoned",
1610
- errorMessage: args.errorMessage ?? existing.errorMessage
1611
- });
1612
- }
1613
- async function failAgentTurnSessionRecord(args) {
1614
- const existing = await getAgentTurnSessionRecord(
1615
- args.conversationId,
1616
- args.sessionId
1617
- );
1618
- if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned" || existing.version !== args.expectedVersion) {
1619
- return void 0;
1620
- }
1621
- return await updateAgentTurnSessionState({
1622
- existing,
1623
- state: "failed",
1624
- errorMessage: args.errorMessage ?? existing.errorMessage
1625
- });
1626
- }
1627
-
1628
- // src/chat/sentry-links.ts
1629
- function getSentryOrgSlug() {
1630
- const slug = process.env.SENTRY_ORG_SLUG?.trim();
1631
- return slug || void 0;
1632
- }
1633
- function isSentrySaasDsnHost(host) {
1634
- return host === "sentry.io" || host.endsWith(".sentry.io");
1635
- }
1636
- function buildSentryWebBaseUrl(dsn) {
1637
- if (isSentrySaasDsnHost(dsn.host)) {
1638
- return "https://sentry.io";
1639
- }
1640
- const port = dsn.port ? `:${dsn.port}` : "";
1641
- const path2 = dsn.path ? `/${dsn.path}` : "";
1642
- return `${dsn.protocol}://${dsn.host}${port}${path2}`;
1643
- }
1644
- function buildSentryConversationUrl(conversationId) {
1645
- const client2 = sentry_exports.getClient();
1646
- const dsn = client2?.getDsn();
1647
- if (!dsn?.host || !dsn.projectId) {
1648
- return void 0;
1649
- }
1650
- const orgSlug = getSentryOrgSlug();
1651
- if (!orgSlug) {
1652
- return void 0;
1653
- }
1654
- const encodedId = encodeURIComponent(conversationId);
1655
- const params = new URLSearchParams();
1656
- params.set("project", dsn.projectId);
1657
- const path2 = `explore/conversations/${encodedId}/?${params.toString()}`;
1658
- if (isSentrySaasDsnHost(dsn.host)) {
1659
- return `https://${orgSlug}.sentry.io/${path2}`;
1660
- }
1661
- return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path2}`;
1662
- }
1663
- function buildSentryTraceUrl(traceId) {
1664
- const client2 = sentry_exports.getClient();
1665
- const dsn = client2?.getDsn();
1666
- if (!dsn?.host || !dsn.projectId) {
1667
- return void 0;
1668
- }
1669
- const orgSlug = getSentryOrgSlug();
1670
- if (!orgSlug) {
1671
- return void 0;
1672
- }
1673
- const encodedTraceId = encodeURIComponent(traceId);
1674
- const params = new URLSearchParams();
1675
- params.set("project", dsn.projectId);
1676
- const path2 = `performance/trace/${encodedTraceId}/?${params.toString()}`;
1677
- if (isSentrySaasDsnHost(dsn.host)) {
1678
- return `https://${orgSlug}.sentry.io/${path2}`;
1679
- }
1680
- return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path2}`;
1681
- }
1682
-
1683
- // src/chat/slack/client.ts
1684
- import { WebClient } from "@slack/web-api";
1685
- var SlackActionError = class extends Error {
1686
- code;
1687
- apiError;
1688
- needed;
1689
- provided;
1690
- statusCode;
1691
- requestId;
1692
- errorData;
1693
- retryAfterSeconds;
1694
- detail;
1695
- detailLine;
1696
- detailRule;
1697
- constructor(message, code, options = {}) {
1698
- super(message);
1699
- this.name = "SlackActionError";
1700
- this.code = code;
1701
- this.apiError = options.apiError;
1702
- this.needed = options.needed;
1703
- this.provided = options.provided;
1704
- this.statusCode = options.statusCode;
1705
- this.requestId = options.requestId;
1706
- this.errorData = options.errorData;
1707
- this.retryAfterSeconds = options.retryAfterSeconds;
1708
- this.detail = options.detail;
1709
- this.detailLine = options.detailLine;
1710
- this.detailRule = options.detailRule;
1711
- }
1712
- };
1713
- function serializeSlackErrorData(data) {
1714
- if (!data || typeof data !== "object") {
1715
- return void 0;
1716
- }
1717
- const filtered = Object.fromEntries(
1718
- Object.entries(data).filter(
1719
- ([key2]) => key2 !== "error"
1720
- )
1721
- );
1722
- if (Object.keys(filtered).length === 0) {
1723
- return void 0;
1724
- }
1725
- try {
1726
- const serialized = JSON.stringify(filtered);
1727
- return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
1728
- } catch {
1729
- return void 0;
1730
- }
1731
- }
1732
- function getHeaderString(headers, name) {
1733
- if (!headers || typeof headers !== "object") {
1734
- return void 0;
1735
- }
1736
- const key2 = name.toLowerCase();
1737
- const entries = headers;
1738
- for (const [entryKey, value] of Object.entries(entries)) {
1739
- if (entryKey.toLowerCase() !== key2) continue;
1740
- if (typeof value === "string") return value;
1741
- if (Array.isArray(value)) {
1742
- const first = value.find((entry) => typeof entry === "string");
1743
- return typeof first === "string" ? first : void 0;
1744
- }
1745
- }
1746
- return void 0;
1747
- }
1748
- function parseSlackCanvasDetail(detail) {
1749
- if (typeof detail !== "string") {
1750
- return {};
1751
- }
1752
- const trimmed = detail.trim();
1753
- if (!trimmed) {
1754
- return {};
1755
- }
1756
- const parsed = {
1757
- detail: trimmed
1758
- };
1759
- const lineMatch = trimmed.match(/line\s+(\d+):/i);
1760
- if (lineMatch) {
1761
- const line = Number.parseInt(lineMatch[1] ?? "", 10);
1762
- if (Number.isFinite(line)) {
1763
- parsed.detailLine = line;
2620
+ continue;
2621
+ }
2622
+ const key3 = `${summary.conversationId}:${summary.sessionId}`;
2623
+ if (!summaries.has(key3)) {
2624
+ summaries.set(key3, summary);
1764
2625
  }
1765
2626
  }
1766
- if (/unsupported heading depth/i.test(trimmed)) {
1767
- parsed.detailRule = "unsupported_heading_depth";
1768
- }
1769
- return parsed;
2627
+ return [...summaries.values()].sort(
2628
+ (left, right) => right.updatedAtMs - left.updatedAtMs
2629
+ );
1770
2630
  }
1771
- var client = null;
1772
- function normalizeSlackConversationId(channelId) {
1773
- if (!channelId) return void 0;
1774
- const trimmed = channelId.trim();
1775
- if (!trimmed) return void 0;
1776
- if (!trimmed.startsWith("slack:")) {
1777
- return trimmed;
1778
- }
1779
- const parts = trimmed.split(":");
1780
- return parts[1]?.trim() || void 0;
2631
+ async function listAgentTurnSessionSummaries(limit = 50) {
2632
+ return (await readAgentTurnSessionSummariesFromIndex(AGENT_TURN_SESSION_INDEX_KEY)).slice(0, Math.max(0, Math.floor(limit)));
1781
2633
  }
1782
- function getClient2() {
1783
- if (client) return client;
1784
- const token = getSlackBotToken();
1785
- if (!token) {
1786
- throw new SlackActionError(
1787
- "SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack canvas/list actions in this service",
1788
- "missing_token"
1789
- );
2634
+ async function listAgentTurnSessionSummariesForConversation(conversationId) {
2635
+ const summaries = await readAgentTurnSessionSummariesFromIndex(
2636
+ agentTurnSessionConversationIndexKey(conversationId)
2637
+ );
2638
+ if (summaries.length > 0) {
2639
+ return summaries;
1790
2640
  }
1791
- client = new WebClient(token);
1792
- return client;
2641
+ return (await readAgentTurnSessionSummariesFromIndex(AGENT_TURN_SESSION_INDEX_KEY)).filter((summary) => summary.conversationId === conversationId);
1793
2642
  }
1794
- function mapSlackError(error) {
1795
- if (error instanceof SlackActionError) {
1796
- return error;
1797
- }
1798
- const candidate = error;
1799
- const apiError = candidate.data?.error;
1800
- const message = candidate.message ?? "Slack action failed";
1801
- const baseOptions = {
1802
- apiError,
1803
- statusCode: candidate.statusCode,
1804
- requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
1805
- errorData: serializeSlackErrorData(candidate.data),
1806
- ...parseSlackCanvasDetail(candidate.data?.detail)
1807
- };
1808
- if (apiError === "missing_scope") {
1809
- return new SlackActionError(message, "missing_scope", {
1810
- ...baseOptions,
1811
- needed: candidate.data?.needed,
1812
- provided: candidate.data?.provided
1813
- });
1814
- }
1815
- if (apiError === "not_in_channel") {
1816
- return new SlackActionError(message, "not_in_channel", baseOptions);
1817
- }
1818
- if (apiError === "already_reacted") {
1819
- return new SlackActionError(message, "already_reacted", baseOptions);
1820
- }
1821
- if (apiError === "no_reaction") {
1822
- return new SlackActionError(message, "no_reaction", baseOptions);
1823
- }
1824
- if (apiError === "invalid_arguments") {
1825
- return new SlackActionError(message, "invalid_arguments", baseOptions);
1826
- }
1827
- if (apiError === "invalid_cursor") {
1828
- return new SlackActionError(message, "invalid_arguments", baseOptions);
1829
- }
1830
- if (apiError === "invalid_name") {
1831
- return new SlackActionError(message, "invalid_arguments", baseOptions);
1832
- }
1833
- if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") {
1834
- return new SlackActionError(message, "not_found", baseOptions);
1835
- }
1836
- if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") {
1837
- return new SlackActionError(message, "feature_unavailable", baseOptions);
1838
- }
1839
- if (apiError === "canvas_creation_failed") {
1840
- return new SlackActionError(message, "canvas_creation_failed", baseOptions);
1841
- }
1842
- if (apiError === "canvas_editing_failed") {
1843
- return new SlackActionError(message, "canvas_editing_failed", baseOptions);
1844
- }
1845
- if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) {
1846
- return new SlackActionError(message, "rate_limited", {
1847
- ...baseOptions,
1848
- retryAfterSeconds: candidate.retryAfter
1849
- });
2643
+ async function abandonAgentTurnSessionRecord(args) {
2644
+ const existing = await getAgentTurnSessionRecord(
2645
+ args.conversationId,
2646
+ args.sessionId
2647
+ );
2648
+ if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned") {
2649
+ return void 0;
1850
2650
  }
1851
- return new SlackActionError(message, "internal_error", baseOptions);
1852
- }
1853
- function sleep(ms) {
1854
- return new Promise((resolve) => setTimeout(resolve, ms));
2651
+ return await updateAgentTurnSessionState({
2652
+ existing,
2653
+ state: "abandoned",
2654
+ errorMessage: args.errorMessage ?? existing.errorMessage
2655
+ });
1855
2656
  }
1856
- async function withSlackRetries(task, maxAttempts = 3, context = {}) {
1857
- let attempt = 0;
1858
- while (attempt < maxAttempts) {
1859
- attempt += 1;
1860
- try {
1861
- return await task();
1862
- } catch (error) {
1863
- const mapped = mapSlackError(error);
1864
- const isRetryable = mapped.code === "rate_limited";
1865
- const baseLogAttributes = {
1866
- "app.slack.action": context.action ?? "unknown",
1867
- "app.slack.error_code": mapped.code,
1868
- ...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : {},
1869
- ...mapped.detail ? { "app.slack.detail": mapped.detail } : {},
1870
- ...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : {},
1871
- ...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : {},
1872
- ...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : {},
1873
- ...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : {},
1874
- ...context.attributes ?? {}
1875
- };
1876
- if (!isRetryable || attempt >= maxAttempts) {
1877
- logWarn(
1878
- "slack_action_failed",
1879
- {},
1880
- {
1881
- ...baseLogAttributes,
1882
- ...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : {}
1883
- },
1884
- "Slack action failed"
1885
- );
1886
- throw mapped;
1887
- }
1888
- logWarn(
1889
- "slack_action_retrying",
1890
- {},
1891
- {
1892
- ...baseLogAttributes,
1893
- "app.slack.retry_attempt": attempt
1894
- },
1895
- "Retrying Slack action after transient failure"
1896
- );
1897
- const retryAfterMs = mapped.code === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
1898
- const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
1899
- await sleep(retryAfterMs ?? backoffMs);
1900
- }
1901
- }
1902
- throw new SlackActionError(
1903
- "Slack action exhausted retries",
1904
- "internal_error"
2657
+ async function failAgentTurnSessionRecord(args) {
2658
+ const existing = await getAgentTurnSessionRecord(
2659
+ args.conversationId,
2660
+ args.sessionId
1905
2661
  );
2662
+ if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned" || existing.version !== args.expectedVersion) {
2663
+ return void 0;
2664
+ }
2665
+ return await updateAgentTurnSessionState({
2666
+ existing,
2667
+ state: "failed",
2668
+ errorMessage: args.errorMessage ?? existing.errorMessage
2669
+ });
1906
2670
  }
1907
- function getSlackClient() {
1908
- return getClient2();
1909
- }
1910
- function isDmChannel(channelId) {
1911
- const normalized = normalizeSlackConversationId(channelId);
1912
- return Boolean(normalized && normalized.startsWith("D"));
2671
+
2672
+ // src/chat/sentry-links.ts
2673
+ function getSentryOrgSlug() {
2674
+ const slug = process.env.SENTRY_ORG_SLUG?.trim();
2675
+ return slug || void 0;
1913
2676
  }
1914
- function isConversationScopedChannel(channelId) {
1915
- const normalized = normalizeSlackConversationId(channelId);
1916
- if (!normalized) return false;
1917
- return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
2677
+ function isSentrySaasDsnHost(host) {
2678
+ return host === "sentry.io" || host.endsWith(".sentry.io");
1918
2679
  }
1919
- function isConversationChannel(channelId) {
1920
- const normalized = normalizeSlackConversationId(channelId);
1921
- if (!normalized) return false;
1922
- return normalized.startsWith("C") || normalized.startsWith("G");
2680
+ function buildSentryWebBaseUrl(dsn) {
2681
+ if (isSentrySaasDsnHost(dsn.host)) {
2682
+ return "https://sentry.io";
2683
+ }
2684
+ const port = dsn.port ? `:${dsn.port}` : "";
2685
+ const path2 = dsn.path ? `/${dsn.path}` : "";
2686
+ return `${dsn.protocol}://${dsn.host}${port}${path2}`;
1923
2687
  }
1924
- async function getFilePermalink(fileId) {
1925
- const client2 = getClient2();
1926
- const response = await withSlackRetries(
1927
- () => client2.files.info({
1928
- file: fileId
1929
- })
1930
- );
1931
- return response.file?.permalink;
2688
+ function buildSentryConversationUrl(conversationId) {
2689
+ const client2 = sentry_exports.getClient();
2690
+ const dsn = client2?.getDsn();
2691
+ if (!dsn?.host || !dsn.projectId) {
2692
+ return void 0;
2693
+ }
2694
+ const orgSlug = getSentryOrgSlug();
2695
+ if (!orgSlug) {
2696
+ return void 0;
2697
+ }
2698
+ const encodedId = encodeURIComponent(conversationId);
2699
+ const params = new URLSearchParams();
2700
+ params.set("project", dsn.projectId);
2701
+ const path2 = `explore/conversations/${encodedId}/?${params.toString()}`;
2702
+ if (isSentrySaasDsnHost(dsn.host)) {
2703
+ return `https://${orgSlug}.sentry.io/${path2}`;
2704
+ }
2705
+ return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path2}`;
1932
2706
  }
1933
- async function downloadPrivateSlackFile(url) {
1934
- const token = getSlackBotToken();
1935
- if (!token) {
1936
- throw new SlackActionError(
1937
- "SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack file downloads in this service",
1938
- "missing_token"
1939
- );
2707
+ function buildSentryTraceUrl(traceId) {
2708
+ const client2 = sentry_exports.getClient();
2709
+ const dsn = client2?.getDsn();
2710
+ if (!dsn?.host || !dsn.projectId) {
2711
+ return void 0;
1940
2712
  }
1941
- const response = await fetch(url, {
1942
- headers: {
1943
- Authorization: `Bearer ${token}`
1944
- }
1945
- });
1946
- if (!response.ok) {
1947
- throw new Error(`Slack file download failed: ${response.status}`);
2713
+ const orgSlug = getSentryOrgSlug();
2714
+ if (!orgSlug) {
2715
+ return void 0;
1948
2716
  }
1949
- return Buffer.from(await response.arrayBuffer());
2717
+ const encodedTraceId = encodeURIComponent(traceId);
2718
+ const params = new URLSearchParams();
2719
+ params.set("project", dsn.projectId);
2720
+ const path2 = `performance/trace/${encodedTraceId}/?${params.toString()}`;
2721
+ if (isSentrySaasDsnHost(dsn.host)) {
2722
+ return `https://${orgSlug}.sentry.io/${path2}`;
2723
+ }
2724
+ return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path2}`;
1950
2725
  }
1951
2726
 
1952
2727
  // src/chat/slack/conversation-context.ts
@@ -2018,6 +2793,8 @@ function formatSlackConversationRedactedLabel(context) {
2018
2793
  }
2019
2794
 
2020
2795
  export {
2796
+ createAgentPluginLogger,
2797
+ createPluginState,
2021
2798
  SlackActionError,
2022
2799
  getHeaderString,
2023
2800
  normalizeSlackConversationId,
@@ -2028,6 +2805,16 @@ export {
2028
2805
  isConversationChannel,
2029
2806
  getFilePermalink,
2030
2807
  downloadPrivateSlackFile,
2808
+ bindSlackDirectCredentialSubject,
2809
+ verifySlackDirectCredentialSubject,
2810
+ validateAgentPlugins,
2811
+ setAgentPlugins,
2812
+ getAgentPlugins,
2813
+ getAgentPluginTools,
2814
+ getAgentPluginRoutes,
2815
+ getAgentPluginSlackConversationLink,
2816
+ getAgentPluginOperationalReports,
2817
+ createAgentPluginHookRunner,
2031
2818
  GET,
2032
2819
  TURN_CONTEXT_TAG,
2033
2820
  getInterruptionMarker,