@sentry/junior 0.4.1 → 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-OZFXD5IG.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
  [
@@ -972,6 +1077,7 @@ function buildSystemPrompt(params) {
972
1077
  "- Use `imageGenerate` when the user asks for image creation.",
973
1078
  "- Use `slackCanvasCreate` for long-form docs/specs and `slackCanvasUpdate` for doc follow-ups.",
974
1079
  "- `slackCanvasUpdate` targets the active artifact-context canvas automatically; do not ask the user for `canvas_id`.",
1080
+ "- When you create or update a Slack artifact in this turn (for example a canvas, list, posted message, or attached file), mention it explicitly in the final reply and include its link when the tool returned one.",
975
1081
  "- Use `slackListCreate`, `slackListAddItems`, and `slackListUpdateItem` for actionable task tracking.",
976
1082
  "- `slackListAddItems`, `slackListGetItems`, and `slackListUpdateItem` target the active artifact-context list automatically; do not ask the user for `list_id`.",
977
1083
  "- If the user explicitly asks to post/send/share/say/show/announce/broadcast in the channel (outside this thread), call `slackChannelPostMessage` with the requested text instead of only replying in-thread.",
@@ -991,7 +1097,7 @@ function buildSystemPrompt(params) {
991
1097
  renderTag(
992
1098
  "skills",
993
1099
  [
994
- "- Explicit skill triggers may appear as `/skillname` or `!skillname`.",
1100
+ "- Explicit skill triggers may appear as `/skillname`.",
995
1101
  "- If explicitly invoked skill instructions are already present in <loaded_skills>, apply them immediately.",
996
1102
  "- Otherwise, for an explicitly invoked skill, call `loadSkill` for that exact skill before applying skill-specific behavior.",
997
1103
  "- For requests without an explicit trigger where a skill clearly matches, call `loadSkill` before applying skill-specific behavior.",
@@ -1020,7 +1126,7 @@ function buildSystemPrompt(params) {
1020
1126
  activeSkillsSection,
1021
1127
  renderTag(
1022
1128
  "invocation-context",
1023
- invocation ? invocation.source === "hard_bang" ? `Explicit skill trigger detected: !${invocation.skillName}` : `Legacy slash hint detected: /${invocation.skillName} (non-authoritative)` : "No explicit skill trigger detected."
1129
+ invocation ? `Explicit skill trigger detected: /${invocation.skillName}` : "No explicit skill trigger detected."
1024
1130
  )
1025
1131
  ];
1026
1132
  return sections.join("\n\n");
@@ -2357,127 +2463,6 @@ import path2 from "path";
2357
2463
  // src/chat/skills.ts
2358
2464
  import fs2 from "fs/promises";
2359
2465
  import path from "path";
2360
-
2361
- // src/chat/skill-frontmatter.ts
2362
- import { parse as parseYaml } from "yaml";
2363
- var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
2364
- var SKILL_NAME_RE = /^[a-z0-9-]+$/;
2365
- var CAPABILITY_TOKEN_RE = /^[a-z0-9]+(?:\.[a-z0-9-]+)+$/;
2366
- var MAX_NAME_LENGTH = 64;
2367
- var MAX_DESCRIPTION_LENGTH = 1024;
2368
- var MAX_COMPATIBILITY_LENGTH = 500;
2369
- function hasAngleBrackets(value) {
2370
- return value.includes("<") || value.includes(">");
2371
- }
2372
- function validateSkillName(name) {
2373
- if (!name) return "name must not be empty";
2374
- if (name.length > MAX_NAME_LENGTH) return `name must be <= ${MAX_NAME_LENGTH} characters`;
2375
- if (!SKILL_NAME_RE.test(name)) return "name must contain only lowercase letters, digits, and hyphens";
2376
- if (name.startsWith("-") || name.endsWith("-")) return "name must not start or end with a hyphen";
2377
- if (name.includes("--")) return "name must not contain consecutive hyphens";
2378
- return null;
2379
- }
2380
- function stripFrontmatter(raw) {
2381
- return raw.replace(FRONTMATTER_RE, "").trim();
2382
- }
2383
- function parseAndValidateSkillFrontmatter(raw, expectedName) {
2384
- const match = FRONTMATTER_RE.exec(raw);
2385
- if (!match) {
2386
- return { ok: false, error: "Missing YAML frontmatter at start of file" };
2387
- }
2388
- let parsed;
2389
- try {
2390
- parsed = parseYaml(match[1]);
2391
- } catch (error) {
2392
- return {
2393
- ok: false,
2394
- error: `Invalid YAML frontmatter: ${error instanceof Error ? error.message : String(error)}`
2395
- };
2396
- }
2397
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2398
- return { ok: false, error: "Frontmatter must be a YAML object" };
2399
- }
2400
- const frontmatter = parsed;
2401
- const name = frontmatter.name;
2402
- const description = frontmatter.description;
2403
- if (typeof name !== "string") {
2404
- return { ok: false, error: 'Frontmatter field "name" must be a string' };
2405
- }
2406
- const nameError = validateSkillName(name);
2407
- if (nameError) {
2408
- return { ok: false, error: nameError };
2409
- }
2410
- if (expectedName && name !== expectedName) {
2411
- return { ok: false, error: `name "${name}" must match directory "${expectedName}"` };
2412
- }
2413
- if (typeof description !== "string") {
2414
- return { ok: false, error: 'Frontmatter field "description" must be a string' };
2415
- }
2416
- if (!description.trim()) {
2417
- return { ok: false, error: "description must not be empty" };
2418
- }
2419
- if (description.length > MAX_DESCRIPTION_LENGTH) {
2420
- return { ok: false, error: `description must be <= ${MAX_DESCRIPTION_LENGTH} characters` };
2421
- }
2422
- if (hasAngleBrackets(description)) {
2423
- return { ok: false, error: 'description must not contain "<" or ">"' };
2424
- }
2425
- if ("metadata" in frontmatter && (typeof frontmatter.metadata !== "object" || !frontmatter.metadata || Array.isArray(frontmatter.metadata))) {
2426
- return { ok: false, error: 'Frontmatter field "metadata" must be an object when present' };
2427
- }
2428
- if ("compatibility" in frontmatter) {
2429
- if (typeof frontmatter.compatibility !== "string") {
2430
- return { ok: false, error: 'Frontmatter field "compatibility" must be a string when present' };
2431
- }
2432
- if (frontmatter.compatibility.length > MAX_COMPATIBILITY_LENGTH) {
2433
- return { ok: false, error: `compatibility must be <= ${MAX_COMPATIBILITY_LENGTH} characters` };
2434
- }
2435
- }
2436
- if ("license" in frontmatter && typeof frontmatter.license !== "string") {
2437
- return { ok: false, error: 'Frontmatter field "license" must be a string when present' };
2438
- }
2439
- if ("allowed-tools" in frontmatter && typeof frontmatter["allowed-tools"] !== "string") {
2440
- return { ok: false, error: 'Frontmatter field "allowed-tools" must be a string when present' };
2441
- }
2442
- if ("requires-capabilities" in frontmatter) {
2443
- if (typeof frontmatter["requires-capabilities"] !== "string") {
2444
- return { ok: false, error: 'Frontmatter field "requires-capabilities" must be a string when present' };
2445
- }
2446
- const tokens = frontmatter["requires-capabilities"].split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0);
2447
- for (const token of tokens) {
2448
- if (!CAPABILITY_TOKEN_RE.test(token)) {
2449
- return {
2450
- ok: false,
2451
- error: `requires-capabilities token "${token}" is invalid; expected dotted lowercase tokens (for example "github.issues.write")`
2452
- };
2453
- }
2454
- }
2455
- }
2456
- if ("uses-config" in frontmatter) {
2457
- if (typeof frontmatter["uses-config"] !== "string") {
2458
- return { ok: false, error: 'Frontmatter field "uses-config" must be a string when present' };
2459
- }
2460
- const tokens = frontmatter["uses-config"].split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0);
2461
- for (const token of tokens) {
2462
- if (!CAPABILITY_TOKEN_RE.test(token)) {
2463
- return {
2464
- ok: false,
2465
- error: `uses-config token "${token}" is invalid; expected dotted lowercase tokens (for example "github.repo")`
2466
- };
2467
- }
2468
- }
2469
- }
2470
- return {
2471
- ok: true,
2472
- frontmatter: {
2473
- ...frontmatter,
2474
- name,
2475
- description
2476
- }
2477
- };
2478
- }
2479
-
2480
- // src/chat/skills.ts
2481
2466
  var SKILL_CACHE_TTL_MS = 5e3;
2482
2467
  var skillCache = null;
2483
2468
  function resolveSkillRoots(options) {
@@ -2487,7 +2472,12 @@ function resolveSkillRoots(options) {
2487
2472
  const pluginRoots = getPluginSkillRoots();
2488
2473
  const seen = /* @__PURE__ */ new Set();
2489
2474
  const resolved = [];
2490
- for (const root of [...additionalRoots, ...envRoots, ...defaults, ...pluginRoots]) {
2475
+ for (const root of [
2476
+ ...additionalRoots,
2477
+ ...envRoots,
2478
+ ...defaults,
2479
+ ...pluginRoots
2480
+ ]) {
2491
2481
  const normalized = path.resolve(root);
2492
2482
  if (seen.has(normalized)) {
2493
2483
  continue;
@@ -2497,22 +2487,6 @@ function resolveSkillRoots(options) {
2497
2487
  }
2498
2488
  return resolved;
2499
2489
  }
2500
- function parseAllowedTools(value) {
2501
- return parseTokenList(value);
2502
- }
2503
- function parseRequiresCapabilities(value) {
2504
- return parseTokenList(value);
2505
- }
2506
- function parseUsesConfig(value) {
2507
- return parseTokenList(value);
2508
- }
2509
- function parseTokenList(value) {
2510
- if (typeof value !== "string") {
2511
- return void 0;
2512
- }
2513
- const parsed = value.split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0);
2514
- return parsed.length > 0 ? parsed : void 0;
2515
- }
2516
2490
  function validateSkillMetadata(input) {
2517
2491
  const unknownCapabilities = (input.requiresCapabilities ?? []).filter(
2518
2492
  (capability) => !isKnownCapability(capability)
@@ -2520,7 +2494,9 @@ function validateSkillMetadata(input) {
2520
2494
  if (unknownCapabilities.length > 0) {
2521
2495
  return `Unknown requires-capabilities values: ${unknownCapabilities.join(", ")}`;
2522
2496
  }
2523
- const unknownConfigKeys = (input.usesConfig ?? []).filter((configKey) => !isKnownConfigKey(configKey));
2497
+ const unknownConfigKeys = (input.usesConfig ?? []).filter(
2498
+ (configKey) => !isKnownConfigKey(configKey)
2499
+ );
2524
2500
  if (unknownConfigKeys.length > 0) {
2525
2501
  return `Unknown uses-config values: ${unknownConfigKeys.join(", ")}`;
2526
2502
  }
@@ -2530,24 +2506,40 @@ async function readSkillDirectory(skillDir) {
2530
2506
  const skillFile = path.join(skillDir, "SKILL.md");
2531
2507
  try {
2532
2508
  const raw = await fs2.readFile(skillFile, "utf8");
2533
- const parsed = parseAndValidateSkillFrontmatter(raw, path.basename(skillDir));
2509
+ const parsed = parseSkillFile(raw, path.basename(skillDir));
2534
2510
  if (!parsed.ok) {
2535
- logWarn("skill_frontmatter_invalid", {}, {
2536
- "file.path": skillDir,
2537
- "error.message": parsed.error
2538
- }, "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
+ );
2539
2520
  return null;
2540
2521
  }
2541
- const { name, description } = parsed.frontmatter;
2542
- const allowedTools = parseAllowedTools(parsed.frontmatter["allowed-tools"]);
2543
- const requiresCapabilities = parseRequiresCapabilities(parsed.frontmatter["requires-capabilities"]);
2544
- const usesConfig = parseUsesConfig(parsed.frontmatter["uses-config"]);
2545
- 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
+ });
2546
2533
  if (metadataError) {
2547
- logWarn("skill_frontmatter_invalid", {}, {
2548
- "file.path": skillDir,
2549
- "error.message": metadataError
2550
- }, "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
+ );
2551
2543
  return null;
2552
2544
  }
2553
2545
  return {
@@ -2559,10 +2551,15 @@ async function readSkillDirectory(skillDir) {
2559
2551
  usesConfig
2560
2552
  };
2561
2553
  } catch (error) {
2562
- logWarn("skill_directory_read_failed", {}, {
2563
- "file.path": skillDir,
2564
- "error.message": error instanceof Error ? error.message : String(error)
2565
- }, "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
+ );
2566
2563
  return null;
2567
2564
  }
2568
2565
  }
@@ -2577,7 +2574,9 @@ async function discoverSkills(options) {
2577
2574
  for (const root of roots) {
2578
2575
  try {
2579
2576
  const entries = await fs2.readdir(root, { withFileTypes: true });
2580
- 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
+ )) {
2581
2580
  if (!entry.isDirectory()) {
2582
2581
  continue;
2583
2582
  }
@@ -2588,10 +2587,15 @@ async function discoverSkills(options) {
2588
2587
  }
2589
2588
  }
2590
2589
  } catch (error) {
2591
- logWarn("skill_root_read_failed", {}, {
2592
- "file.directory": root,
2593
- "error.message": error instanceof Error ? error.message : String(error)
2594
- }, "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
+ );
2595
2599
  }
2596
2600
  }
2597
2601
  const sorted = discovered.sort((a, b) => a.name.localeCompare(b.name));
@@ -2604,31 +2608,20 @@ async function discoverSkills(options) {
2604
2608
  }
2605
2609
  function parseSkillInvocation(messageText, availableSkills) {
2606
2610
  const trimmed = messageText.trim();
2607
- const toInvocation = (match, source) => {
2608
- if (!match) {
2609
- return null;
2610
- }
2611
- const skillName = match[1].toLowerCase();
2612
- if (!availableSkills.some((skill) => skill.name === skillName)) {
2613
- return null;
2614
- }
2615
- return {
2616
- skillName,
2617
- args: (match[2] ?? "").trim(),
2618
- source
2619
- };
2620
- };
2621
- const hardBangInvocation = toInvocation(
2622
- /(?:^|\s)!([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s+([\s\S]*))?/i.exec(trimmed),
2623
- "hard_bang"
2611
+ const match = /(?:^|\s)\/([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s+([\s\S]*))?/i.exec(
2612
+ trimmed
2624
2613
  );
2625
- if (hardBangInvocation) {
2626
- return hardBangInvocation;
2614
+ if (!match) {
2615
+ return null;
2627
2616
  }
2628
- return toInvocation(
2629
- /(?:^|\s)\/([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s+([\s\S]*))?/i.exec(trimmed),
2630
- "legacy_slash"
2631
- );
2617
+ const skillName = match[1].toLowerCase();
2618
+ if (!availableSkills.some((skill) => skill.name === skillName)) {
2619
+ return null;
2620
+ }
2621
+ return {
2622
+ skillName,
2623
+ args: (match[2] ?? "").trim()
2624
+ };
2632
2625
  }
2633
2626
  function findSkillByName(skillName, available) {
2634
2627
  return available.find((skill) => skill.name === skillName) ?? null;
@@ -2642,9 +2635,13 @@ async function loadSkillsByName(skillNames, available) {
2642
2635
  }
2643
2636
  const skillFile = path.join(meta.skillPath, "SKILL.md");
2644
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
+ }
2645
2642
  skills.push({
2646
2643
  ...meta,
2647
- body: stripFrontmatter(raw)
2644
+ body: parsed.skill.body
2648
2645
  });
2649
2646
  }
2650
2647
  return skills;
@@ -3349,19 +3346,11 @@ function toLoadedSkill(result) {
3349
3346
  body: result.instructions
3350
3347
  };
3351
3348
  }
3352
- function stripFrontmatter2(raw) {
3353
- if (!raw.startsWith("---")) {
3354
- return raw;
3355
- }
3356
- const match = /^---\n[\s\S]*?\n---\n?/.exec(raw);
3357
- if (!match) {
3358
- return raw;
3359
- }
3360
- return raw.slice(match[0].length);
3361
- }
3362
3349
  async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
3363
3350
  const requested = skillName.trim().toLowerCase();
3364
- const skill = availableSkills.find((entry) => entry.name.toLowerCase() === requested);
3351
+ const skill = availableSkills.find(
3352
+ (entry) => entry.name.toLowerCase() === requested
3353
+ );
3365
3354
  if (!skill) {
3366
3355
  return {
3367
3356
  ok: false,
@@ -3381,7 +3370,7 @@ async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
3381
3370
  description: skill.description,
3382
3371
  skill_dir: skillDir,
3383
3372
  location: skillFilePath,
3384
- instructions: stripFrontmatter2(file.toString("utf8"))
3373
+ instructions: stripFrontmatter(file.toString("utf8"))
3385
3374
  };
3386
3375
  }
3387
3376
  function createLoadSkillTool(sandbox, availableSkills, options) {
@@ -3394,7 +3383,11 @@ function createLoadSkillTool(sandbox, availableSkills, options) {
3394
3383
  })
3395
3384
  }),
3396
3385
  execute: async ({ skill_name }) => {
3397
- const result = await loadSkillFromSandbox(sandbox, availableSkills, skill_name);
3386
+ const result = await loadSkillFromSandbox(
3387
+ sandbox,
3388
+ availableSkills,
3389
+ skill_name
3390
+ );
3398
3391
  const loadedSkill = toLoadedSkill(result);
3399
3392
  if (loadedSkill) {
3400
3393
  await options?.onSkillLoaded?.(loadedSkill);
@@ -5552,14 +5545,14 @@ function createSandboxExecutor(options) {
5552
5545
  } : {}
5553
5546
  });
5554
5547
  if (!snapshot.snapshotId) {
5555
- await emitSandboxStatus("Starting sandbox...");
5548
+ await emitSandboxStatus("Booting up...");
5556
5549
  return await Sandbox.create({
5557
5550
  timeout: timeoutMs,
5558
5551
  runtime
5559
5552
  });
5560
5553
  }
5561
5554
  try {
5562
- await emitSandboxStatus("Starting sandbox from snapshot...");
5555
+ await emitSandboxStatus("Booting up...");
5563
5556
  return await Sandbox.create({
5564
5557
  timeout: timeoutMs,
5565
5558
  source: {
@@ -5585,7 +5578,7 @@ function createSandboxExecutor(options) {
5585
5578
  throw error;
5586
5579
  }
5587
5580
  await emitSandboxStatus(
5588
- "Retrying sandbox startup with a fresh snapshot..."
5581
+ "Booting up..."
5589
5582
  );
5590
5583
  return await Sandbox.create({
5591
5584
  timeout: timeoutMs,
@@ -6197,9 +6190,9 @@ function formatToolStatus(toolName) {
6197
6190
  const known = {
6198
6191
  loadSkill: "Loading skill instructions",
6199
6192
  systemTime: "Reading current system time",
6200
- bash: "Running shell command in sandbox",
6201
- readFile: "Reading file in sandbox",
6202
- writeFile: "Writing file in sandbox",
6193
+ bash: "Working in the shell",
6194
+ readFile: "Reading a file",
6195
+ writeFile: "Updating a file",
6203
6196
  webSearch: "Searching public sources",
6204
6197
  webFetch: "Reading source pages",
6205
6198
  slackChannelPostMessage: "Posting message to channel",
@@ -6220,16 +6213,23 @@ function formatToolStatus(toolName) {
6220
6213
  }
6221
6214
  function formatToolStatusWithInput(toolName, input) {
6222
6215
  const obj = input && typeof input === "object" ? input : void 0;
6216
+ const command = obj ? compactStatusText(obj.command, 70) : void 0;
6223
6217
  const path6 = obj ? compactStatusPath(obj.path) : void 0;
6224
6218
  const filename = obj ? compactStatusFilename(obj.path) : void 0;
6225
6219
  const query = obj ? compactStatusText(obj.query, 70) : void 0;
6226
6220
  const domain = obj ? extractStatusUrlDomain(obj.url) : void 0;
6227
6221
  const skillName = obj ? compactStatusText(obj.skill_name ?? obj.skillName, 40) : void 0;
6222
+ if (command && toolName === "bash") {
6223
+ return `Running ${command}`;
6224
+ }
6228
6225
  if (filename && toolName === "readFile") {
6229
6226
  return `Reading file ${filename}`;
6230
6227
  }
6228
+ if (filename && toolName === "writeFile") {
6229
+ return `Updating file ${filename}`;
6230
+ }
6231
6231
  if (path6 && toolName === "writeFile") {
6232
- return `Writing file ${path6}`;
6232
+ return `Updating file ${path6}`;
6233
6233
  }
6234
6234
  if (skillName && toolName === "loadSkill") {
6235
6235
  return `Loading skill ${skillName}`;
@@ -6246,7 +6246,7 @@ function formatToolResultStatus(toolName) {
6246
6246
  const known = {
6247
6247
  loadSkill: "Integrating loaded skill guidance",
6248
6248
  systemTime: "Applying current time context",
6249
- bash: "Analyzing command output",
6249
+ bash: "Reviewing command results",
6250
6250
  readFile: "Analyzing file contents",
6251
6251
  writeFile: "Saving file update",
6252
6252
  webSearch: "Reviewing search results",
@@ -6269,16 +6269,23 @@ function formatToolResultStatus(toolName) {
6269
6269
  }
6270
6270
  function formatToolResultStatusWithInput(toolName, input) {
6271
6271
  const obj = input && typeof input === "object" ? input : void 0;
6272
+ const command = obj ? compactStatusText(obj.command, 70) : void 0;
6272
6273
  const path6 = obj ? compactStatusPath(obj.path) : void 0;
6273
6274
  const filename = obj ? compactStatusFilename(obj.path) : void 0;
6274
6275
  const query = obj ? compactStatusText(obj.query, 70) : void 0;
6275
6276
  const domain = obj ? extractStatusUrlDomain(obj.url) : void 0;
6276
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
+ }
6277
6281
  if (filename && toolName === "readFile") {
6278
6282
  return `Reviewed file ${filename}`;
6279
6283
  }
6284
+ if (filename && toolName === "writeFile") {
6285
+ return `Updated file ${filename}`;
6286
+ }
6280
6287
  if (path6 && toolName === "writeFile") {
6281
- return `Saved file ${path6}`;
6288
+ return `Updated file ${path6}`;
6282
6289
  }
6283
6290
  if (skillName && toolName === "loadSkill") {
6284
6291
  return `Loaded skill ${skillName}`;
@@ -6749,7 +6756,7 @@ async function generateAssistantReply(messageText, context = {}) {
6749
6756
  lastKnownSandboxDependencyProfileHash = sandboxExecutor.getDependencyProfileHash();
6750
6757
  sandboxExecutor.configureSkills(availableSkills);
6751
6758
  const sandbox = await sandboxExecutor.createSandbox();
6752
- if (invokedSkill && skillInvocation?.source === "hard_bang") {
6759
+ if (invokedSkill) {
6753
6760
  const preloaded = await skillSandbox.loadSkill(invokedSkill.name);
6754
6761
  if (preloaded) {
6755
6762
  activeSkills.push(preloaded);
@@ -7420,7 +7427,7 @@ function resolveSlackChannelIdFromMessage(message) {
7420
7427
  }
7421
7428
 
7422
7429
  // src/chat/runtime/thread-context.ts
7423
- function escapeRegExp(value) {
7430
+ function escapeRegExp2(value) {
7424
7431
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7425
7432
  }
7426
7433
  function stripLeadingBotMention(text, options = {}) {
@@ -7429,10 +7436,10 @@ function stripLeadingBotMention(text, options = {}) {
7429
7436
  if (options.stripLeadingSlackMentionToken) {
7430
7437
  next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
7431
7438
  }
7432
- const mentionByNameRe = new RegExp(`^\\s*@${escapeRegExp(botConfig.userName)}\\b[\\s,:-]*`, "i");
7439
+ const mentionByNameRe = new RegExp(`^\\s*@${escapeRegExp2(botConfig.userName)}\\b[\\s,:-]*`, "i");
7433
7440
  next = next.replace(mentionByNameRe, "").trim();
7434
7441
  const mentionByLabeledEntityRe = new RegExp(
7435
- `^\\s*<@[^>|]+\\|${escapeRegExp(botConfig.userName)}>[\\s,:-]*`,
7442
+ `^\\s*<@[^>|]+\\|${escapeRegExp2(botConfig.userName)}>[\\s,:-]*`,
7436
7443
  "i"
7437
7444
  );
7438
7445
  next = next.replace(mentionByLabeledEntityRe, "").trim();
@@ -8031,18 +8038,13 @@ function createAppSlackRuntime(deps) {
8031
8038
  requesterUserName: message.author.userName,
8032
8039
  runId
8033
8040
  });
8034
- await deps.withSpan(
8035
- "chat.turn",
8036
- "chat.turn",
8037
- context,
8038
- async () => {
8039
- await thread.subscribe();
8040
- await deps.replyToThread(thread, message, {
8041
- explicitMention: true,
8042
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost
8043
- });
8044
- }
8045
- );
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
+ });
8046
8048
  } catch (error) {
8047
8049
  const errorContext = logContext({
8048
8050
  threadId: deps.getThreadId(thread, message),
@@ -8095,6 +8097,45 @@ function createAppSlackRuntime(deps) {
8095
8097
  channelId,
8096
8098
  runId
8097
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
+ }
8098
8139
  const preparedState = await deps.prepareTurnState({
8099
8140
  thread,
8100
8141
  message,
@@ -8277,7 +8318,7 @@ async function buildSkillsSummaryText() {
8277
8318
  return "No skills installed.";
8278
8319
  }
8279
8320
  const visible = skills.slice(0, MAX_HOME_SKILLS);
8280
- const lines = visible.map((skill) => `\u2022 \`!${skill.name}\` \u2014 ${skill.description}`);
8321
+ const lines = visible.map((skill) => `\u2022 *${skill.name}* \u2014 ${skill.description}`);
8281
8322
  if (skills.length > visible.length) {
8282
8323
  lines.push(`\u2022 \u2026and ${skills.length - visible.length} more`);
8283
8324
  }
@@ -10242,11 +10283,58 @@ var appSlackRuntime = createAppSlackRuntime({
10242
10283
  },
10243
10284
  getPreparedConversationContext: (preparedState) => preparedState.routingContext ?? preparedState.conversationContext,
10244
10285
  shouldReplyInSubscribedThread,
10245
- onSubscribedMessageSkipped: async ({ thread, preparedState, decision, completedAtMs }) => {
10246
- markConversationMessage(preparedState.conversation, preparedState.userMessageId, {
10247
- replied: false,
10248
- 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
10249
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
+ );
10250
10338
  preparedState.conversation.processing.activeTurnId = void 0;
10251
10339
  preparedState.conversation.processing.lastCompletedAtMs = completedAtMs;
10252
10340
  updateConversationStats(preparedState.conversation);
@@ -10255,7 +10343,12 @@ var appSlackRuntime = createAppSlackRuntime({
10255
10343
  });
10256
10344
  },
10257
10345
  replyToThread,
10258
- initializeAssistantThread: async ({ threadId, channelId, threadTs, sourceChannelId }) => {
10346
+ initializeAssistantThread: async ({
10347
+ threadId,
10348
+ channelId,
10349
+ threadTs,
10350
+ sourceChannelId
10351
+ }) => {
10259
10352
  await initializeAssistantThread({
10260
10353
  threadId,
10261
10354
  channelId,