@sentry/junior 0.5.0 → 0.6.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.
@@ -1,7 +1,12 @@
1
+ import {
2
+ parseSkillFile,
3
+ stripFrontmatter
4
+ } from "./chunk-KT5HARSN.js";
1
5
  import {
2
6
  CredentialUnavailableError,
3
7
  SANDBOX_SKILLS_ROOT,
4
8
  SANDBOX_WORKSPACE_ROOT,
9
+ aboutPathCandidates,
5
10
  botConfig,
6
11
  claimQueueIngressDedup,
7
12
  createPluginBroker,
@@ -26,7 +31,7 @@ import {
26
31
  skillRoots,
27
32
  soulPathCandidates,
28
33
  upsertAgentTurnSessionCheckpoint
29
- } from "./chunk-BKYYVLVN.js";
34
+ } from "./chunk-5UJSQX4R.js";
30
35
  import {
31
36
  logError,
32
37
  logException,
@@ -281,6 +286,11 @@ var ACK_REGEXES = [
281
286
  ];
282
287
  var QUESTION_PREFIX_RE = /^(what|why|how|when|where|which|who|can|could|would|should|do|does|did|is|are|was|were|will)\b/i;
283
288
  var FOLLOW_UP_REF_RE = /\b(you|your|that|this|it|above|previous|earlier|last|just\s+said)\b/i;
289
+ var LEADING_SLACK_MENTION_RE = /^\s*<@([A-Z0-9]+)(?:\|([^>]+))?>[\s,:-]*/i;
290
+ var LEADING_NAMED_MENTION_RE = /^\s*@([a-z0-9._-]+)\b[\s,:-]*/i;
291
+ function escapeRegExp(value) {
292
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
293
+ }
284
294
  function tokenizeForOverlap(value) {
285
295
  return value.toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length >= 4);
286
296
  }
@@ -329,6 +339,54 @@ function isLikelyAssistantDirectedFollowUp(text, conversationContext) {
329
339
  }
330
340
  return false;
331
341
  }
342
+ function containsAssistantInvocation(text, botUserName) {
343
+ const escapedUserName = escapeRegExp(botUserName);
344
+ const plainNameMentionRe = new RegExp(`(^|\\s)@${escapedUserName}\\b`, "i");
345
+ const labeledEntityMentionRe = new RegExp(
346
+ `<@[^>|]+\\|${escapedUserName}>`,
347
+ "i"
348
+ );
349
+ return plainNameMentionRe.test(text) || labeledEntityMentionRe.test(text);
350
+ }
351
+ function detectLeadingOtherPartyAddress(rawText, text, botUserName) {
352
+ if (containsAssistantInvocation(rawText, botUserName) || containsAssistantInvocation(text, botUserName)) {
353
+ return void 0;
354
+ }
355
+ const leadingSlackMention = rawText.match(LEADING_SLACK_MENTION_RE);
356
+ if (leadingSlackMention) {
357
+ const label = leadingSlackMention[2]?.trim();
358
+ return label ? `slack_mention:${label}` : "slack_mention";
359
+ }
360
+ const leadingNamedMention = text.match(LEADING_NAMED_MENTION_RE);
361
+ if (!leadingNamedMention) {
362
+ return void 0;
363
+ }
364
+ const directedName = leadingNamedMention[1]?.trim();
365
+ if (!directedName || directedName.toLowerCase() === botUserName.toLowerCase()) {
366
+ return void 0;
367
+ }
368
+ return `named_mention:${directedName}`;
369
+ }
370
+ function getSubscribedReplyPreflightDecision(args) {
371
+ const text = args.text.trim();
372
+ const rawText = args.rawText.trim();
373
+ if (args.isExplicitMention) {
374
+ return { shouldReply: true, reason: "explicit_mention" /* ExplicitMention */ };
375
+ }
376
+ const leadingOtherPartyAddress = detectLeadingOtherPartyAddress(
377
+ rawText,
378
+ text,
379
+ args.botUserName
380
+ );
381
+ if (!leadingOtherPartyAddress) {
382
+ return void 0;
383
+ }
384
+ return {
385
+ shouldReply: false,
386
+ reason: "directed_to_other_party" /* DirectedToOtherParty */,
387
+ reasonDetail: leadingOtherPartyAddress
388
+ };
389
+ }
332
390
  function buildRouterSystemPrompt(botUserName, conversationContext) {
333
391
  return [
334
392
  "You are a message router for a Slack assistant named Junior in a subscribed Slack thread.",
@@ -352,8 +410,14 @@ function buildRouterSystemPrompt(botUserName, conversationContext) {
352
410
  async function decideSubscribedThreadReply(args) {
353
411
  const text = args.input.text.trim();
354
412
  const rawText = args.input.rawText.trim();
355
- if (args.input.isExplicitMention) {
356
- return { shouldReply: true, reason: "explicit_mention" /* ExplicitMention */ };
413
+ const preflightDecision = getSubscribedReplyPreflightDecision({
414
+ botUserName: args.botUserName,
415
+ rawText,
416
+ text,
417
+ isExplicitMention: args.input.isExplicitMention
418
+ });
419
+ if (preflightDecision) {
420
+ return preflightDecision;
357
421
  }
358
422
  if (!text && !args.input.hasAttachments) {
359
423
  return { shouldReply: false, reason: "empty_message" /* EmptyMessage */ };
@@ -365,7 +429,10 @@ async function decideSubscribedThreadReply(args) {
365
429
  return { shouldReply: false, reason: "acknowledgment" /* Acknowledgment */ };
366
430
  }
367
431
  if (isLikelyAssistantDirectedFollowUp(text, args.input.conversationContext)) {
368
- return { shouldReply: true, reason: "follow_up_question" /* FollowUpQuestion */ };
432
+ return {
433
+ shouldReply: true,
434
+ reason: "follow_up_question" /* FollowUpQuestion */
435
+ };
369
436
  }
370
437
  try {
371
438
  const result = await args.completeObject({
@@ -373,7 +440,10 @@ async function decideSubscribedThreadReply(args) {
373
440
  schema: replyDecisionSchema,
374
441
  maxTokens: 120,
375
442
  temperature: 0,
376
- system: buildRouterSystemPrompt(args.botUserName, args.input.conversationContext),
443
+ system: buildRouterSystemPrompt(
444
+ args.botUserName,
445
+ args.input.conversationContext
446
+ ),
377
447
  prompt: rawText,
378
448
  metadata: {
379
449
  modelId: args.modelId,
@@ -694,20 +764,20 @@ var slackOutputPolicy = {
694
764
 
695
765
  // src/chat/prompt.ts
696
766
  var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
697
- function loadSoul() {
767
+ function loadOptionalMarkdownFile(candidates, fileName) {
698
768
  const attempted = [];
699
- for (const resolved of soulPathCandidates()) {
769
+ for (const resolved of candidates) {
700
770
  attempted.push(resolved);
701
771
  try {
702
772
  const raw = fs.readFileSync(resolved, "utf8").trim();
703
773
  if (raw.length > 0) {
704
774
  logInfo(
705
- "soul_loaded",
775
+ `${fileName.toLowerCase()}_loaded`,
706
776
  {},
707
777
  {
708
778
  "file.path": resolved
709
779
  },
710
- "Loaded SOUL.md"
780
+ `Loaded ${fileName}`
711
781
  );
712
782
  return raw;
713
783
  }
@@ -715,16 +785,26 @@ function loadSoul() {
715
785
  continue;
716
786
  }
717
787
  }
788
+ return null;
789
+ }
790
+ function loadSoul() {
791
+ const soul = loadOptionalMarkdownFile(soulPathCandidates(), "SOUL.md");
792
+ if (soul) {
793
+ return soul;
794
+ }
718
795
  logWarn(
719
796
  "soul_load_fallback",
720
797
  {},
721
798
  {
722
- "file.candidates": attempted
799
+ "file.candidates": soulPathCandidates()
723
800
  },
724
801
  "SOUL.md not found; using built-in default personality"
725
802
  );
726
803
  return DEFAULT_SOUL;
727
804
  }
805
+ function loadAbout() {
806
+ return loadOptionalMarkdownFile(aboutPathCandidates(), "ABOUT.md");
807
+ }
728
808
  var JUNIOR_PERSONALITY = (() => {
729
809
  try {
730
810
  return loadSoul();
@@ -740,6 +820,21 @@ var JUNIOR_PERSONALITY = (() => {
740
820
  return DEFAULT_SOUL;
741
821
  }
742
822
  })();
823
+ var JUNIOR_ABOUT = (() => {
824
+ try {
825
+ return loadAbout();
826
+ } catch (error) {
827
+ logWarn(
828
+ "about_load_failed",
829
+ {},
830
+ {
831
+ "error.message": error instanceof Error ? error.message : String(error)
832
+ },
833
+ "Failed to load ABOUT.md; omitting about prompt context"
834
+ );
835
+ return null;
836
+ }
837
+ })();
743
838
  function workspaceSkillDir(skillName) {
744
839
  return sandboxSkillDir(skillName);
745
840
  }
@@ -912,6 +1007,16 @@ function buildSystemPrompt(params) {
912
1007
  JUNIOR_PERSONALITY.trim()
913
1008
  ].join("\n")
914
1009
  ),
1010
+ ...JUNIOR_ABOUT ? [
1011
+ renderTag(
1012
+ "about",
1013
+ [
1014
+ "Use this as the assistant's product/domain description when relevant.",
1015
+ "",
1016
+ JUNIOR_ABOUT.trim()
1017
+ ].join("\n")
1018
+ )
1019
+ ] : [],
915
1020
  renderTag(
916
1021
  "identity-context",
917
1022
  [
@@ -2358,127 +2463,6 @@ import path2 from "path";
2358
2463
  // src/chat/skills.ts
2359
2464
  import fs2 from "fs/promises";
2360
2465
  import path from "path";
2361
-
2362
- // src/chat/skill-frontmatter.ts
2363
- import { parse as parseYaml } from "yaml";
2364
- var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
2365
- var SKILL_NAME_RE = /^[a-z0-9-]+$/;
2366
- var CAPABILITY_TOKEN_RE = /^[a-z0-9]+(?:\.[a-z0-9-]+)+$/;
2367
- var MAX_NAME_LENGTH = 64;
2368
- var MAX_DESCRIPTION_LENGTH = 1024;
2369
- var MAX_COMPATIBILITY_LENGTH = 500;
2370
- function hasAngleBrackets(value) {
2371
- return value.includes("<") || value.includes(">");
2372
- }
2373
- function validateSkillName(name) {
2374
- if (!name) return "name must not be empty";
2375
- if (name.length > MAX_NAME_LENGTH) return `name must be <= ${MAX_NAME_LENGTH} characters`;
2376
- if (!SKILL_NAME_RE.test(name)) return "name must contain only lowercase letters, digits, and hyphens";
2377
- if (name.startsWith("-") || name.endsWith("-")) return "name must not start or end with a hyphen";
2378
- if (name.includes("--")) return "name must not contain consecutive hyphens";
2379
- return null;
2380
- }
2381
- function stripFrontmatter(raw) {
2382
- return raw.replace(FRONTMATTER_RE, "").trim();
2383
- }
2384
- function parseAndValidateSkillFrontmatter(raw, expectedName) {
2385
- const match = FRONTMATTER_RE.exec(raw);
2386
- if (!match) {
2387
- return { ok: false, error: "Missing YAML frontmatter at start of file" };
2388
- }
2389
- let parsed;
2390
- try {
2391
- parsed = parseYaml(match[1]);
2392
- } catch (error) {
2393
- return {
2394
- ok: false,
2395
- error: `Invalid YAML frontmatter: ${error instanceof Error ? error.message : String(error)}`
2396
- };
2397
- }
2398
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2399
- return { ok: false, error: "Frontmatter must be a YAML object" };
2400
- }
2401
- const frontmatter = parsed;
2402
- const name = frontmatter.name;
2403
- const description = frontmatter.description;
2404
- if (typeof name !== "string") {
2405
- return { ok: false, error: 'Frontmatter field "name" must be a string' };
2406
- }
2407
- const nameError = validateSkillName(name);
2408
- if (nameError) {
2409
- return { ok: false, error: nameError };
2410
- }
2411
- if (expectedName && name !== expectedName) {
2412
- return { ok: false, error: `name "${name}" must match directory "${expectedName}"` };
2413
- }
2414
- if (typeof description !== "string") {
2415
- return { ok: false, error: 'Frontmatter field "description" must be a string' };
2416
- }
2417
- if (!description.trim()) {
2418
- return { ok: false, error: "description must not be empty" };
2419
- }
2420
- if (description.length > MAX_DESCRIPTION_LENGTH) {
2421
- return { ok: false, error: `description must be <= ${MAX_DESCRIPTION_LENGTH} characters` };
2422
- }
2423
- if (hasAngleBrackets(description)) {
2424
- return { ok: false, error: 'description must not contain "<" or ">"' };
2425
- }
2426
- if ("metadata" in frontmatter && (typeof frontmatter.metadata !== "object" || !frontmatter.metadata || Array.isArray(frontmatter.metadata))) {
2427
- return { ok: false, error: 'Frontmatter field "metadata" must be an object when present' };
2428
- }
2429
- if ("compatibility" in frontmatter) {
2430
- if (typeof frontmatter.compatibility !== "string") {
2431
- return { ok: false, error: 'Frontmatter field "compatibility" must be a string when present' };
2432
- }
2433
- if (frontmatter.compatibility.length > MAX_COMPATIBILITY_LENGTH) {
2434
- return { ok: false, error: `compatibility must be <= ${MAX_COMPATIBILITY_LENGTH} characters` };
2435
- }
2436
- }
2437
- if ("license" in frontmatter && typeof frontmatter.license !== "string") {
2438
- return { ok: false, error: 'Frontmatter field "license" must be a string when present' };
2439
- }
2440
- if ("allowed-tools" in frontmatter && typeof frontmatter["allowed-tools"] !== "string") {
2441
- return { ok: false, error: 'Frontmatter field "allowed-tools" must be a string when present' };
2442
- }
2443
- if ("requires-capabilities" in frontmatter) {
2444
- if (typeof frontmatter["requires-capabilities"] !== "string") {
2445
- return { ok: false, error: 'Frontmatter field "requires-capabilities" must be a string when present' };
2446
- }
2447
- const tokens = frontmatter["requires-capabilities"].split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0);
2448
- for (const token of tokens) {
2449
- if (!CAPABILITY_TOKEN_RE.test(token)) {
2450
- return {
2451
- ok: false,
2452
- error: `requires-capabilities token "${token}" is invalid; expected dotted lowercase tokens (for example "github.issues.write")`
2453
- };
2454
- }
2455
- }
2456
- }
2457
- if ("uses-config" in frontmatter) {
2458
- if (typeof frontmatter["uses-config"] !== "string") {
2459
- return { ok: false, error: 'Frontmatter field "uses-config" must be a string when present' };
2460
- }
2461
- const tokens = frontmatter["uses-config"].split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0);
2462
- for (const token of tokens) {
2463
- if (!CAPABILITY_TOKEN_RE.test(token)) {
2464
- return {
2465
- ok: false,
2466
- error: `uses-config token "${token}" is invalid; expected dotted lowercase tokens (for example "github.repo")`
2467
- };
2468
- }
2469
- }
2470
- }
2471
- return {
2472
- ok: true,
2473
- frontmatter: {
2474
- ...frontmatter,
2475
- name,
2476
- description
2477
- }
2478
- };
2479
- }
2480
-
2481
- // src/chat/skills.ts
2482
2466
  var SKILL_CACHE_TTL_MS = 5e3;
2483
2467
  var skillCache = null;
2484
2468
  function resolveSkillRoots(options) {
@@ -2488,7 +2472,12 @@ function resolveSkillRoots(options) {
2488
2472
  const pluginRoots = getPluginSkillRoots();
2489
2473
  const seen = /* @__PURE__ */ new Set();
2490
2474
  const resolved = [];
2491
- for (const root of [...additionalRoots, ...envRoots, ...defaults, ...pluginRoots]) {
2475
+ for (const root of [
2476
+ ...additionalRoots,
2477
+ ...envRoots,
2478
+ ...defaults,
2479
+ ...pluginRoots
2480
+ ]) {
2492
2481
  const normalized = path.resolve(root);
2493
2482
  if (seen.has(normalized)) {
2494
2483
  continue;
@@ -2498,22 +2487,6 @@ function resolveSkillRoots(options) {
2498
2487
  }
2499
2488
  return resolved;
2500
2489
  }
2501
- function parseAllowedTools(value) {
2502
- return parseTokenList(value);
2503
- }
2504
- function parseRequiresCapabilities(value) {
2505
- return parseTokenList(value);
2506
- }
2507
- function parseUsesConfig(value) {
2508
- return parseTokenList(value);
2509
- }
2510
- function parseTokenList(value) {
2511
- if (typeof value !== "string") {
2512
- return void 0;
2513
- }
2514
- const parsed = value.split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0);
2515
- return parsed.length > 0 ? parsed : void 0;
2516
- }
2517
2490
  function validateSkillMetadata(input) {
2518
2491
  const unknownCapabilities = (input.requiresCapabilities ?? []).filter(
2519
2492
  (capability) => !isKnownCapability(capability)
@@ -2521,7 +2494,9 @@ function validateSkillMetadata(input) {
2521
2494
  if (unknownCapabilities.length > 0) {
2522
2495
  return `Unknown requires-capabilities values: ${unknownCapabilities.join(", ")}`;
2523
2496
  }
2524
- const unknownConfigKeys = (input.usesConfig ?? []).filter((configKey) => !isKnownConfigKey(configKey));
2497
+ const unknownConfigKeys = (input.usesConfig ?? []).filter(
2498
+ (configKey) => !isKnownConfigKey(configKey)
2499
+ );
2525
2500
  if (unknownConfigKeys.length > 0) {
2526
2501
  return `Unknown uses-config values: ${unknownConfigKeys.join(", ")}`;
2527
2502
  }
@@ -2531,24 +2506,40 @@ async function readSkillDirectory(skillDir) {
2531
2506
  const skillFile = path.join(skillDir, "SKILL.md");
2532
2507
  try {
2533
2508
  const raw = await fs2.readFile(skillFile, "utf8");
2534
- const parsed = parseAndValidateSkillFrontmatter(raw, path.basename(skillDir));
2509
+ const parsed = parseSkillFile(raw, path.basename(skillDir));
2535
2510
  if (!parsed.ok) {
2536
- logWarn("skill_frontmatter_invalid", {}, {
2537
- "file.path": skillDir,
2538
- "error.message": parsed.error
2539
- }, "Invalid skill frontmatter");
2511
+ logWarn(
2512
+ "skill_frontmatter_invalid",
2513
+ {},
2514
+ {
2515
+ "file.path": skillDir,
2516
+ "error.message": parsed.error
2517
+ },
2518
+ "Invalid skill frontmatter"
2519
+ );
2540
2520
  return null;
2541
2521
  }
2542
- const { name, description } = parsed.frontmatter;
2543
- const allowedTools = parseAllowedTools(parsed.frontmatter["allowed-tools"]);
2544
- const requiresCapabilities = parseRequiresCapabilities(parsed.frontmatter["requires-capabilities"]);
2545
- const usesConfig = parseUsesConfig(parsed.frontmatter["uses-config"]);
2546
- const metadataError = validateSkillMetadata({ requiresCapabilities, usesConfig });
2522
+ const {
2523
+ name,
2524
+ description,
2525
+ allowedTools,
2526
+ requiresCapabilities,
2527
+ usesConfig
2528
+ } = parsed.skill;
2529
+ const metadataError = validateSkillMetadata({
2530
+ requiresCapabilities,
2531
+ usesConfig
2532
+ });
2547
2533
  if (metadataError) {
2548
- logWarn("skill_frontmatter_invalid", {}, {
2549
- "file.path": skillDir,
2550
- "error.message": metadataError
2551
- }, "Invalid skill frontmatter");
2534
+ logWarn(
2535
+ "skill_frontmatter_invalid",
2536
+ {},
2537
+ {
2538
+ "file.path": skillDir,
2539
+ "error.message": metadataError
2540
+ },
2541
+ "Invalid skill frontmatter"
2542
+ );
2552
2543
  return null;
2553
2544
  }
2554
2545
  return {
@@ -2560,10 +2551,15 @@ async function readSkillDirectory(skillDir) {
2560
2551
  usesConfig
2561
2552
  };
2562
2553
  } catch (error) {
2563
- logWarn("skill_directory_read_failed", {}, {
2564
- "file.path": skillDir,
2565
- "error.message": error instanceof Error ? error.message : String(error)
2566
- }, "Failed to read skill directory");
2554
+ logWarn(
2555
+ "skill_directory_read_failed",
2556
+ {},
2557
+ {
2558
+ "file.path": skillDir,
2559
+ "error.message": error instanceof Error ? error.message : String(error)
2560
+ },
2561
+ "Failed to read skill directory"
2562
+ );
2567
2563
  return null;
2568
2564
  }
2569
2565
  }
@@ -2578,7 +2574,9 @@ async function discoverSkills(options) {
2578
2574
  for (const root of roots) {
2579
2575
  try {
2580
2576
  const entries = await fs2.readdir(root, { withFileTypes: true });
2581
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
2577
+ for (const entry of entries.sort(
2578
+ (a, b) => a.name.localeCompare(b.name)
2579
+ )) {
2582
2580
  if (!entry.isDirectory()) {
2583
2581
  continue;
2584
2582
  }
@@ -2589,10 +2587,15 @@ async function discoverSkills(options) {
2589
2587
  }
2590
2588
  }
2591
2589
  } catch (error) {
2592
- logWarn("skill_root_read_failed", {}, {
2593
- "file.directory": root,
2594
- "error.message": error instanceof Error ? error.message : String(error)
2595
- }, "Failed to read skill root");
2590
+ logWarn(
2591
+ "skill_root_read_failed",
2592
+ {},
2593
+ {
2594
+ "file.directory": root,
2595
+ "error.message": error instanceof Error ? error.message : String(error)
2596
+ },
2597
+ "Failed to read skill root"
2598
+ );
2596
2599
  }
2597
2600
  }
2598
2601
  const sorted = discovered.sort((a, b) => a.name.localeCompare(b.name));
@@ -2605,7 +2608,9 @@ async function discoverSkills(options) {
2605
2608
  }
2606
2609
  function parseSkillInvocation(messageText, availableSkills) {
2607
2610
  const trimmed = messageText.trim();
2608
- const match = /(?:^|\s)\/([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s+([\s\S]*))?/i.exec(trimmed);
2611
+ const match = /(?:^|\s)\/([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s+([\s\S]*))?/i.exec(
2612
+ trimmed
2613
+ );
2609
2614
  if (!match) {
2610
2615
  return null;
2611
2616
  }
@@ -2630,9 +2635,13 @@ async function loadSkillsByName(skillNames, available) {
2630
2635
  }
2631
2636
  const skillFile = path.join(meta.skillPath, "SKILL.md");
2632
2637
  const raw = await fs2.readFile(skillFile, "utf8");
2638
+ const parsed = parseSkillFile(raw, meta.name);
2639
+ if (!parsed.ok) {
2640
+ throw new Error(`Invalid skill file in ${skillFile}: ${parsed.error}`);
2641
+ }
2633
2642
  skills.push({
2634
2643
  ...meta,
2635
- body: stripFrontmatter(raw)
2644
+ body: parsed.skill.body
2636
2645
  });
2637
2646
  }
2638
2647
  return skills;
@@ -3337,19 +3346,11 @@ function toLoadedSkill(result) {
3337
3346
  body: result.instructions
3338
3347
  };
3339
3348
  }
3340
- function stripFrontmatter2(raw) {
3341
- if (!raw.startsWith("---")) {
3342
- return raw;
3343
- }
3344
- const match = /^---\n[\s\S]*?\n---\n?/.exec(raw);
3345
- if (!match) {
3346
- return raw;
3347
- }
3348
- return raw.slice(match[0].length);
3349
- }
3350
3349
  async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
3351
3350
  const requested = skillName.trim().toLowerCase();
3352
- const skill = availableSkills.find((entry) => entry.name.toLowerCase() === requested);
3351
+ const skill = availableSkills.find(
3352
+ (entry) => entry.name.toLowerCase() === requested
3353
+ );
3353
3354
  if (!skill) {
3354
3355
  return {
3355
3356
  ok: false,
@@ -3369,7 +3370,7 @@ async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
3369
3370
  description: skill.description,
3370
3371
  skill_dir: skillDir,
3371
3372
  location: skillFilePath,
3372
- instructions: stripFrontmatter2(file.toString("utf8"))
3373
+ instructions: stripFrontmatter(file.toString("utf8"))
3373
3374
  };
3374
3375
  }
3375
3376
  function createLoadSkillTool(sandbox, availableSkills, options) {
@@ -3382,7 +3383,11 @@ function createLoadSkillTool(sandbox, availableSkills, options) {
3382
3383
  })
3383
3384
  }),
3384
3385
  execute: async ({ skill_name }) => {
3385
- const result = await loadSkillFromSandbox(sandbox, availableSkills, skill_name);
3386
+ const result = await loadSkillFromSandbox(
3387
+ sandbox,
3388
+ availableSkills,
3389
+ skill_name
3390
+ );
3386
3391
  const loadedSkill = toLoadedSkill(result);
3387
3392
  if (loadedSkill) {
3388
3393
  await options?.onSkillLoaded?.(loadedSkill);
@@ -5540,14 +5545,14 @@ function createSandboxExecutor(options) {
5540
5545
  } : {}
5541
5546
  });
5542
5547
  if (!snapshot.snapshotId) {
5543
- await emitSandboxStatus("Starting sandbox...");
5548
+ await emitSandboxStatus("Booting up...");
5544
5549
  return await Sandbox.create({
5545
5550
  timeout: timeoutMs,
5546
5551
  runtime
5547
5552
  });
5548
5553
  }
5549
5554
  try {
5550
- await emitSandboxStatus("Starting sandbox from snapshot...");
5555
+ await emitSandboxStatus("Booting up...");
5551
5556
  return await Sandbox.create({
5552
5557
  timeout: timeoutMs,
5553
5558
  source: {
@@ -5573,7 +5578,7 @@ function createSandboxExecutor(options) {
5573
5578
  throw error;
5574
5579
  }
5575
5580
  await emitSandboxStatus(
5576
- "Retrying sandbox startup with a fresh snapshot..."
5581
+ "Booting up..."
5577
5582
  );
5578
5583
  return await Sandbox.create({
5579
5584
  timeout: timeoutMs,
@@ -6185,9 +6190,9 @@ function formatToolStatus(toolName) {
6185
6190
  const known = {
6186
6191
  loadSkill: "Loading skill instructions",
6187
6192
  systemTime: "Reading current system time",
6188
- bash: "Running shell command in sandbox",
6189
- readFile: "Reading file in sandbox",
6190
- writeFile: "Writing file in sandbox",
6193
+ bash: "Working in the shell",
6194
+ readFile: "Reading a file",
6195
+ writeFile: "Updating a file",
6191
6196
  webSearch: "Searching public sources",
6192
6197
  webFetch: "Reading source pages",
6193
6198
  slackChannelPostMessage: "Posting message to channel",
@@ -6208,16 +6213,23 @@ function formatToolStatus(toolName) {
6208
6213
  }
6209
6214
  function formatToolStatusWithInput(toolName, input) {
6210
6215
  const obj = input && typeof input === "object" ? input : void 0;
6216
+ const command = obj ? compactStatusText(obj.command, 70) : void 0;
6211
6217
  const path6 = obj ? compactStatusPath(obj.path) : void 0;
6212
6218
  const filename = obj ? compactStatusFilename(obj.path) : void 0;
6213
6219
  const query = obj ? compactStatusText(obj.query, 70) : void 0;
6214
6220
  const domain = obj ? extractStatusUrlDomain(obj.url) : void 0;
6215
6221
  const skillName = obj ? compactStatusText(obj.skill_name ?? obj.skillName, 40) : void 0;
6222
+ if (command && toolName === "bash") {
6223
+ return `Running ${command}`;
6224
+ }
6216
6225
  if (filename && toolName === "readFile") {
6217
6226
  return `Reading file ${filename}`;
6218
6227
  }
6228
+ if (filename && toolName === "writeFile") {
6229
+ return `Updating file ${filename}`;
6230
+ }
6219
6231
  if (path6 && toolName === "writeFile") {
6220
- return `Writing file ${path6}`;
6232
+ return `Updating file ${path6}`;
6221
6233
  }
6222
6234
  if (skillName && toolName === "loadSkill") {
6223
6235
  return `Loading skill ${skillName}`;
@@ -6234,7 +6246,7 @@ function formatToolResultStatus(toolName) {
6234
6246
  const known = {
6235
6247
  loadSkill: "Integrating loaded skill guidance",
6236
6248
  systemTime: "Applying current time context",
6237
- bash: "Analyzing command output",
6249
+ bash: "Reviewing command results",
6238
6250
  readFile: "Analyzing file contents",
6239
6251
  writeFile: "Saving file update",
6240
6252
  webSearch: "Reviewing search results",
@@ -6257,16 +6269,23 @@ function formatToolResultStatus(toolName) {
6257
6269
  }
6258
6270
  function formatToolResultStatusWithInput(toolName, input) {
6259
6271
  const obj = input && typeof input === "object" ? input : void 0;
6272
+ const command = obj ? compactStatusText(obj.command, 70) : void 0;
6260
6273
  const path6 = obj ? compactStatusPath(obj.path) : void 0;
6261
6274
  const filename = obj ? compactStatusFilename(obj.path) : void 0;
6262
6275
  const query = obj ? compactStatusText(obj.query, 70) : void 0;
6263
6276
  const domain = obj ? extractStatusUrlDomain(obj.url) : void 0;
6264
6277
  const skillName = obj ? compactStatusText(obj.skill_name ?? obj.skillName, 40) : void 0;
6278
+ if (command && toolName === "bash") {
6279
+ return `Reviewed results from ${command}`;
6280
+ }
6265
6281
  if (filename && toolName === "readFile") {
6266
6282
  return `Reviewed file ${filename}`;
6267
6283
  }
6284
+ if (filename && toolName === "writeFile") {
6285
+ return `Updated file ${filename}`;
6286
+ }
6268
6287
  if (path6 && toolName === "writeFile") {
6269
- return `Saved file ${path6}`;
6288
+ return `Updated file ${path6}`;
6270
6289
  }
6271
6290
  if (skillName && toolName === "loadSkill") {
6272
6291
  return `Loaded skill ${skillName}`;
@@ -7408,7 +7427,7 @@ function resolveSlackChannelIdFromMessage(message) {
7408
7427
  }
7409
7428
 
7410
7429
  // src/chat/runtime/thread-context.ts
7411
- function escapeRegExp(value) {
7430
+ function escapeRegExp2(value) {
7412
7431
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7413
7432
  }
7414
7433
  function stripLeadingBotMention(text, options = {}) {
@@ -7417,10 +7436,10 @@ function stripLeadingBotMention(text, options = {}) {
7417
7436
  if (options.stripLeadingSlackMentionToken) {
7418
7437
  next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
7419
7438
  }
7420
- const mentionByNameRe = new RegExp(`^\\s*@${escapeRegExp(botConfig.userName)}\\b[\\s,:-]*`, "i");
7439
+ const mentionByNameRe = new RegExp(`^\\s*@${escapeRegExp2(botConfig.userName)}\\b[\\s,:-]*`, "i");
7421
7440
  next = next.replace(mentionByNameRe, "").trim();
7422
7441
  const mentionByLabeledEntityRe = new RegExp(
7423
- `^\\s*<@[^>|]+\\|${escapeRegExp(botConfig.userName)}>[\\s,:-]*`,
7442
+ `^\\s*<@[^>|]+\\|${escapeRegExp2(botConfig.userName)}>[\\s,:-]*`,
7424
7443
  "i"
7425
7444
  );
7426
7445
  next = next.replace(mentionByLabeledEntityRe, "").trim();
@@ -8019,18 +8038,13 @@ function createAppSlackRuntime(deps) {
8019
8038
  requesterUserName: message.author.userName,
8020
8039
  runId
8021
8040
  });
8022
- await deps.withSpan(
8023
- "chat.turn",
8024
- "chat.turn",
8025
- context,
8026
- async () => {
8027
- await thread.subscribe();
8028
- await deps.replyToThread(thread, message, {
8029
- explicitMention: true,
8030
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost
8031
- });
8032
- }
8033
- );
8041
+ await deps.withSpan("chat.turn", "chat.turn", context, async () => {
8042
+ await thread.subscribe();
8043
+ await deps.replyToThread(thread, message, {
8044
+ explicitMention: true,
8045
+ beforeFirstResponsePost: hooks?.beforeFirstResponsePost
8046
+ });
8047
+ });
8034
8048
  } catch (error) {
8035
8049
  const errorContext = logContext({
8036
8050
  threadId: deps.getThreadId(thread, message),
@@ -8083,6 +8097,45 @@ function createAppSlackRuntime(deps) {
8083
8097
  channelId,
8084
8098
  runId
8085
8099
  };
8100
+ const preflightDecision = hooks?.preApprovedReply ? void 0 : getSubscribedReplyPreflightDecision({
8101
+ botUserName: deps.assistantUserName,
8102
+ rawText: rawUserText,
8103
+ text: userText,
8104
+ isExplicitMention: Boolean(message.isMention)
8105
+ });
8106
+ if (preflightDecision && !preflightDecision.shouldReply) {
8107
+ const completedAtMs = deps.now();
8108
+ const reason = preflightDecision.reasonDetail ? `${preflightDecision.reason}:${preflightDecision.reasonDetail}` : preflightDecision.reason;
8109
+ deps.logWarn(
8110
+ "subscribed_message_reply_skipped",
8111
+ logContext({
8112
+ threadId,
8113
+ requesterId: message.author.userId,
8114
+ requesterUserName: message.author.userName,
8115
+ channelId,
8116
+ runId
8117
+ }),
8118
+ {
8119
+ "app.decision.reason": reason
8120
+ },
8121
+ "Skipping subscribed message reply"
8122
+ );
8123
+ await deps.onSubscribedMessageSkipped({
8124
+ thread,
8125
+ message,
8126
+ decision: { shouldReply: false, reason },
8127
+ completedAtMs,
8128
+ preparedState: void 0
8129
+ });
8130
+ await deps.recordSkippedSubscribedMessage({
8131
+ thread,
8132
+ message,
8133
+ decision: { shouldReply: false, reason },
8134
+ completedAtMs,
8135
+ userText
8136
+ });
8137
+ return;
8138
+ }
8086
8139
  const preparedState = await deps.prepareTurnState({
8087
8140
  thread,
8088
8141
  message,
@@ -10230,11 +10283,58 @@ var appSlackRuntime = createAppSlackRuntime({
10230
10283
  },
10231
10284
  getPreparedConversationContext: (preparedState) => preparedState.routingContext ?? preparedState.conversationContext,
10232
10285
  shouldReplyInSubscribedThread,
10233
- onSubscribedMessageSkipped: async ({ thread, preparedState, decision, completedAtMs }) => {
10234
- markConversationMessage(preparedState.conversation, preparedState.userMessageId, {
10235
- replied: false,
10236
- skippedReason: decision.reason
10286
+ recordSkippedSubscribedMessage: async ({
10287
+ thread,
10288
+ message,
10289
+ decision,
10290
+ completedAtMs,
10291
+ userText
10292
+ }) => {
10293
+ const conversation = coerceThreadConversationState(await thread.state);
10294
+ const normalizedUserText = normalizeConversationText(userText) || "[non-text message]";
10295
+ upsertConversationMessage(conversation, {
10296
+ id: message.id,
10297
+ role: "user",
10298
+ text: normalizedUserText,
10299
+ createdAtMs: message.metadata.dateSent.getTime(),
10300
+ author: {
10301
+ userId: message.author.userId,
10302
+ userName: message.author.userName,
10303
+ fullName: message.author.fullName,
10304
+ isBot: typeof message.author.isBot === "boolean" ? message.author.isBot : void 0
10305
+ },
10306
+ meta: {
10307
+ explicitMention: Boolean(message.isMention),
10308
+ slackTs: message.id,
10309
+ replied: false,
10310
+ skippedReason: decision.reason,
10311
+ imagesHydrated: true
10312
+ }
10313
+ });
10314
+ conversation.processing.activeTurnId = void 0;
10315
+ conversation.processing.lastCompletedAtMs = completedAtMs;
10316
+ updateConversationStats(conversation);
10317
+ await persistThreadState(thread, {
10318
+ conversation
10237
10319
  });
10320
+ },
10321
+ onSubscribedMessageSkipped: async ({
10322
+ thread,
10323
+ preparedState,
10324
+ decision,
10325
+ completedAtMs
10326
+ }) => {
10327
+ if (!preparedState) {
10328
+ return;
10329
+ }
10330
+ markConversationMessage(
10331
+ preparedState.conversation,
10332
+ preparedState.userMessageId,
10333
+ {
10334
+ replied: false,
10335
+ skippedReason: decision.reason
10336
+ }
10337
+ );
10238
10338
  preparedState.conversation.processing.activeTurnId = void 0;
10239
10339
  preparedState.conversation.processing.lastCompletedAtMs = completedAtMs;
10240
10340
  updateConversationStats(preparedState.conversation);
@@ -10243,7 +10343,12 @@ var appSlackRuntime = createAppSlackRuntime({
10243
10343
  });
10244
10344
  },
10245
10345
  replyToThread,
10246
- initializeAssistantThread: async ({ threadId, channelId, threadTs, sourceChannelId }) => {
10346
+ initializeAssistantThread: async ({
10347
+ threadId,
10348
+ channelId,
10349
+ threadTs,
10350
+ sourceChannelId
10351
+ }) => {
10247
10352
  await initializeAssistantThread({
10248
10353
  threadId,
10249
10354
  channelId,