@skillrecordings/cli 0.20.0 → 0.21.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.
package/README.md CHANGED
@@ -7,8 +7,10 @@ Agent-friendly CLI for the Skill Recordings support platform.
7
7
  You need: a GitHub account in the `skillrecordings` org.
8
8
 
9
9
  ```bash
10
- # 1. Install
11
- curl -fsSL https://raw.githubusercontent.com/skillrecordings/support/main/packages/cli/install.sh | bash
10
+ # 1. Install (private repo, so use gh)
11
+ gh auth login
12
+ gh api -H "Accept: application/vnd.github.raw" \
13
+ repos/skillrecordings/support/contents/packages/cli/install.sh?ref=main | bash
12
14
 
13
15
  # 2. Add to PATH (if not already)
14
16
  echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc
@@ -25,6 +27,23 @@ That's it. No 1Password CLI, no age keys, no manual secret management.
25
27
  The CLI authenticates via GitHub device flow → the broker verifies your org
26
28
  membership → secrets are delivered encrypted and held in memory only.
27
29
 
30
+ ## Install troubleshooting
31
+
32
+ | Symptom | Fix |
33
+ |---|---|
34
+ | `gh: command not found` | Install GitHub CLI: https://cli.github.com/ |
35
+ | `gh` asks you to log in | Run `gh auth login`, then `gh auth status` |
36
+ | `HTTP 404` / repo access failure on bootstrap | Confirm you can access the private repo: `gh release list --repo skillrecordings/support --limit 5` |
37
+ | `skill: command not found` after install | Add `~/.local/bin` to PATH |
38
+
39
+ Quick verification:
40
+
41
+ ```bash
42
+ gh auth status
43
+ which skill
44
+ skill --version
45
+ ```
46
+
28
47
  ## Commands
29
48
 
30
49
  ```bash
@@ -14,7 +14,7 @@ import {
14
14
  } from "./chunk-HKRLO2GE.js";
15
15
  import {
16
16
  queryVectors
17
- } from "./chunk-H3D6VCME.js";
17
+ } from "./chunk-ZZGGSV6N.js";
18
18
  import {
19
19
  Index2
20
20
  } from "./chunk-MG37YDAK.js";
@@ -81,7 +81,9 @@ function computeMessageSignals(body, subject = "") {
81
81
  isPersonalToInstructor: PERSONAL_MESSAGE_PATTERN.test(text) || INSTRUCTOR_MENTION_PATTERN.test(text) && /(?:thank|love|amazing|big fan)/i.test(text),
82
82
  // Presales signals (set by LLM classifier, defaulted here)
83
83
  isPresalesFaq: false,
84
- isPresalesTeam: false
84
+ isPresalesTeam: false,
85
+ isThirdPartyReceipt: false,
86
+ isKnownVendorDomain: false
85
87
  };
86
88
  }
87
89
  function computeThreadSignals(input) {
@@ -165,6 +167,8 @@ function computeThreadSignals(input) {
165
167
  // Presales signals (merged from base)
166
168
  isPresalesFaq: baseSignals.isPresalesFaq || threadWideSignals.isPresalesFaq,
167
169
  isPresalesTeam: baseSignals.isPresalesTeam || threadWideSignals.isPresalesTeam,
170
+ isThirdPartyReceipt: false,
171
+ isKnownVendorDomain: false,
168
172
  // Thread structure
169
173
  threadLength: sorted.length,
170
174
  threadDurationHours,
@@ -283,6 +287,24 @@ var VENDOR_PATTERNS = [
283
287
  /\bproduction[- ]ready\s+(?:saas|app|template|boilerplate|platform|starter)/i
284
288
  // cold SaaS pitch
285
289
  ];
290
+ var THIRD_PARTY_RECEIPT_PATTERNS = [
291
+ /\b(?:invoice|receipt|payment)\b.*\b(?:fly\.io|upstash|dmarc|mux|inngest|vercel|github|heroku|aws|digitalocean|cloudflare|netlify|render)\b/i,
292
+ /\b(?:fly\.io|upstash|dmarc|mux|inngest)\b.*\b(?:invoice|receipt|payment|billing|subscription|charge)\b/i,
293
+ /your\s+(?:monthly|annual|weekly)\s+(?:invoice|receipt|statement)\s+(?:from|for)\b/i
294
+ ];
295
+ var KNOWN_VENDOR_DOMAINS = [
296
+ "fly.io",
297
+ "upstash.com",
298
+ "dmarcdigests.com",
299
+ "dmarc.postmarkapp.com",
300
+ "mux.com",
301
+ "inngest.com",
302
+ "gaming1.com",
303
+ "guestposting",
304
+ "meecocreative.com",
305
+ "getrecall.ai",
306
+ "seaverse.ai"
307
+ ];
286
308
  var LEGAL_THREAT_PATTERNS = [
287
309
  /\b(?:my\s+)?lawyer/i,
288
310
  /\blegal\s+action/i,
@@ -404,6 +426,9 @@ function extractSignals(input) {
404
426
  hasAngrySentiment: ANGRY_PATTERNS.some((p) => p.test(text)),
405
427
  isAutomated: AUTOMATED_PATTERNS.some((p) => p.test(text)) || (input.from ? /noreply|no-reply|mailer-daemon/i.test(input.from) : false),
406
428
  isVendorOutreach: VENDOR_PATTERNS.some((p) => p.test(text)),
429
+ // ADR-0003 D2: Third-party receipts and known vendor domains
430
+ isThirdPartyReceipt: THIRD_PARTY_RECEIPT_PATTERNS.some((p) => p.test(text)),
431
+ isKnownVendorDomain: input.from ? KNOWN_VENDOR_DOMAINS.some((d) => input.from.toLowerCase().includes(d)) : false,
407
432
  // Escalation signals
408
433
  hasLegalThreat: LEGAL_THREAT_PATTERNS.some((p) => p.test(fullText)),
409
434
  hasOutsidePolicyTimeframe: OUTSIDE_POLICY_PATTERNS.some(
@@ -429,6 +454,15 @@ function fastClassify(input, signals) {
429
454
  _fastPathRule: "automated_system"
430
455
  };
431
456
  }
457
+ if (signals.isThirdPartyReceipt || signals.isKnownVendorDomain) {
458
+ return {
459
+ category: "system",
460
+ confidence: 0.95,
461
+ signals,
462
+ reasoning: signals.isKnownVendorDomain ? `Sender domain matches known vendor/service (${input.from})` : "Third-party receipt/notification \u2014 not a customer support request",
463
+ _fastPathRule: signals.isKnownVendorDomain ? "known_vendor_domain" : "third_party_receipt"
464
+ };
465
+ }
432
466
  if (signals.isVendorOutreach && !signals.hasEmailInBody) {
433
467
  return {
434
468
  category: "spam",
@@ -1467,8 +1501,10 @@ var EditDraftSchema = z2.object({
1467
1501
  cc: z2.array(z2.string()).optional(),
1468
1502
  bcc: z2.array(z2.string()).optional(),
1469
1503
  subject: z2.string().optional(),
1470
- version: z2.string()
1504
+ version: z2.string(),
1471
1505
  // Required for optimistic locking
1506
+ channel_id: z2.string().optional()
1507
+ // Required by Front API for PATCH
1472
1508
  });
1473
1509
  var MessageTemplateFolderSchema = z2.object({
1474
1510
  _links: z2.object({
@@ -2318,6 +2354,12 @@ function createInstrumentedFrontClient(config) {
2318
2354
  }
2319
2355
 
2320
2356
  // ../core/src/pipeline/steps/comment.ts
2357
+ var REASONING_MAX_LENGTH = 150;
2358
+ function truncateReasoning(text, maxLength = REASONING_MAX_LENGTH) {
2359
+ if (!text) return text;
2360
+ if (text.length <= maxLength) return text;
2361
+ return text.slice(0, maxLength).trimEnd() + "\u2026";
2362
+ }
2321
2363
  function formatSupportComment(context) {
2322
2364
  const parts = ["\u{1F916} **Agent Research Context**\n"];
2323
2365
  if (context.user) {
@@ -2406,121 +2448,47 @@ function formatEscalationType(type) {
2406
2448
  return "Escalated";
2407
2449
  }
2408
2450
  }
2409
- function formatPurchase(purchase, adminBaseUrl) {
2410
- const status = purchase.status !== "active" ? ` (${purchase.status})` : "";
2411
- const amount = purchase.amount ? ` - $${(purchase.amount / 100).toFixed(2)}` : "";
2412
- if (adminBaseUrl && purchase.productId) {
2413
- const adminLink = `${adminBaseUrl}/purchases/${purchase.productId}`;
2414
- return `- [${purchase.productName}](${adminLink})${amount}${status}`;
2415
- }
2416
- return `- ${purchase.productName}${amount}${status}`;
2417
- }
2418
2451
  function formatEscalationComment(context) {
2419
- const parts = [];
2420
2452
  const emoji = getEscalationEmoji(context.type);
2421
2453
  const typeLabel = formatEscalationType(context.type);
2422
- parts.push(`${emoji} **Agent Escalation (${typeLabel})**
2423
- `);
2424
- parts.push(`**Reason:** ${context.reason}`);
2425
- if (context.classification) {
2426
- parts.push(`**Category:** ${context.classification.category}`);
2427
- parts.push(
2428
- `**Confidence:** ${Math.round(context.classification.confidence * 100)}%`
2429
- );
2430
- if (context.classification.reasoning) {
2431
- parts.push(`**Agent reasoning:** ${context.classification.reasoning}`);
2432
- }
2433
- }
2434
- parts.push("\n---");
2435
- parts.push("**Customer Info**");
2436
- parts.push(`- **Email:** ${context.customer.email}`);
2437
- if (context.customer.name) {
2438
- parts.push(`- **Name:** ${context.customer.name}`);
2439
- }
2440
- if (context.customer.id) {
2441
- parts.push(`- **ID:** ${context.customer.id}`);
2442
- }
2443
- parts.push("\n**Purchases:**");
2444
- if (context.purchases && context.purchases.length > 0) {
2445
- const adminBaseUrl = context.links?.admin?.replace(/\/users\/.*$/, "");
2446
- for (const p of context.purchases) {
2447
- parts.push(formatPurchase(p, adminBaseUrl));
2448
- }
2449
- } else {
2450
- parts.push("_No purchases found for this email_");
2451
- }
2452
- if (context.agentFindings && context.agentFindings.length > 0) {
2453
- parts.push("\n**What Agent Found/Tried:**");
2454
- for (const finding of context.agentFindings) {
2455
- parts.push(`- ${finding}`);
2456
- }
2457
- }
2458
- if (context.links) {
2459
- const hasLinks = context.links.admin || context.links.magicLogin || context.links.frontConversation;
2460
- if (hasLinks) {
2461
- parts.push("\n---");
2462
- parts.push("**Quick Links**");
2463
- if (context.links.admin) {
2464
- parts.push(`- [Admin Profile](${context.links.admin})`);
2465
- }
2466
- if (context.links.magicLogin) {
2467
- parts.push(`- [Magic Login](${context.links.magicLogin})`);
2468
- }
2469
- if (context.links.frontConversation) {
2470
- parts.push(`- [Front Conversation](${context.links.frontConversation})`);
2471
- }
2472
- }
2454
+ const parts = [];
2455
+ parts.push(`${emoji} **${typeLabel}:** ${context.reason}`);
2456
+ const purchaseCount = context.purchases?.length ?? 0;
2457
+ const purchaseSummary = purchaseCount > 0 ? `${purchaseCount} purchase${purchaseCount > 1 ? "s" : ""}` : "no purchases";
2458
+ parts.push(`${context.customer.email} (${purchaseSummary})`);
2459
+ if (context.links?.magicLogin) {
2460
+ parts.push(`[Magic Login](${context.links.magicLogin})`);
2473
2461
  }
2474
2462
  return parts.join("\n");
2475
2463
  }
2476
2464
  function formatApprovalComment(context) {
2477
- const parts = [];
2478
- parts.push("\u{1F50D} **Draft Pending Review**\n");
2479
- parts.push(`**Review Reason:** ${context.reviewReason}`);
2480
2465
  const confidencePercent = Math.round(context.confidence * 100);
2481
- const confidenceEmoji = confidencePercent >= 80 ? "\u{1F7E2}" : confidencePercent >= 60 ? "\u{1F7E1}" : "\u{1F534}";
2482
- parts.push(`**Confidence:** ${confidenceEmoji} ${confidencePercent}%`);
2483
- if (context.category) {
2484
- parts.push(`**Category:** ${context.category}`);
2485
- }
2466
+ const parts = [];
2467
+ const cat = context.category ? `${context.category} ` : "";
2468
+ parts.push(
2469
+ `\u{1F50D} **Review:** ${cat}(${confidencePercent}%) \u2014 ${context.reviewReason}`
2470
+ );
2486
2471
  if (context.customerEmail) {
2487
- parts.push(`**Customer:** ${context.customerEmail}`);
2488
- }
2489
- parts.push("\n---");
2490
- parts.push("**Draft Preview:**");
2491
- parts.push("");
2492
- const draftLines = context.draft.split("\n");
2493
- for (const line of draftLines) {
2494
- parts.push(`> ${line}`);
2472
+ parts.push(context.customerEmail);
2495
2473
  }
2496
2474
  if (context.actionLinks) {
2497
- parts.push("\n---");
2498
- parts.push("**Actions:**");
2475
+ const links = [];
2499
2476
  if (context.actionLinks.approve) {
2500
- parts.push(`- [\u2705 Approve & Send](${context.actionLinks.approve})`);
2477
+ links.push(`[Approve](${context.actionLinks.approve})`);
2501
2478
  }
2502
2479
  if (context.actionLinks.edit) {
2503
- parts.push(`- [\u270F\uFE0F Edit Draft](${context.actionLinks.edit})`);
2480
+ links.push(`[Edit](${context.actionLinks.edit})`);
2481
+ }
2482
+ if (links.length > 0) {
2483
+ parts.push(links.join(" \xB7 "));
2504
2484
  }
2505
2485
  }
2506
2486
  return parts.join("\n");
2507
2487
  }
2508
2488
  function formatAuditComment(context) {
2509
- const parts = [];
2510
2489
  const actionEmoji = context.action === "auto_sent" ? "\u2705" : context.action === "draft_created" ? "\u{1F4DD}" : context.action === "silenced" ? "\u{1F507}" : context.action === "escalated" ? "\u26A0\uFE0F" : "\u{1F916}";
2511
- parts.push(
2512
- `${actionEmoji} **Agent Action: ${formatActionLabel(context.action)}**`
2513
- );
2514
2490
  const confidencePercent = Math.round(context.confidence * 100);
2515
- parts.push(
2516
- `Category: ${context.category} | Confidence: ${confidencePercent}%`
2517
- );
2518
- const timestamp = context.timestamp ?? /* @__PURE__ */ new Date();
2519
- parts.push(`Timestamp: ${timestamp.toISOString()}`);
2520
- if (context.messageId) {
2521
- parts.push(`Message ID: ${context.messageId}`);
2522
- }
2523
- return parts.join("\n");
2491
+ return `${actionEmoji} ${formatActionLabel(context.action)} \u2014 ${context.category} (${confidencePercent}%)`;
2524
2492
  }
2525
2493
  function formatActionLabel(action) {
2526
2494
  switch (action) {
@@ -2573,7 +2541,7 @@ function formatDecisionComment(context) {
2573
2541
  const confidencePercent = Math.round(context.confidence * 100);
2574
2542
  parts.push(`**Category:** ${context.category} (${confidencePercent}%)`);
2575
2543
  if (context.reasoning) {
2576
- parts.push(`**Reasoning:** ${context.reasoning}`);
2544
+ parts.push(`**Reasoning:** ${truncateReasoning(context.reasoning)}`);
2577
2545
  }
2578
2546
  if (context.customerEmail) {
2579
2547
  const customerLine = context.customerName ? `${context.customerName} (${context.customerEmail})` : context.customerEmail;
@@ -2760,12 +2728,13 @@ init_esm_shims();
2760
2728
  var BASE_DRAFT_PROMPT = `You are a support agent. Write a helpful response to the customer.
2761
2729
 
2762
2730
  ## Style Guide
2763
- - Be direct and concise
2764
- - No corporate speak
2765
- - No enthusiasm performance ("Great!", "Happy to help!")
2731
+ - Be direct and concise \u2014 1-3 sentences is ideal, not paragraphs
2732
+ - Give the answer or link first, explain only if needed
2733
+ - No corporate speak, no enthusiasm performance
2766
2734
  - Get to the point immediately
2767
- - If you need info, just ask - no softening
2768
- - 2-3 short paragraphs max
2735
+ - If you need info, just ask \u2014 no softening
2736
+ - NEVER use numbered step-by-step lists for simple questions. A direct link + 1 sentence beats a 5-step guide every time.
2737
+ - NEVER say "I've escalated this" or "someone will reach out" \u2014 if you have a URL or answer, give it directly
2769
2738
 
2770
2739
  ## NEVER Use These Phrases
2771
2740
  - "Great!" or exclamatory openers
@@ -2776,6 +2745,14 @@ var BASE_DRAFT_PROMPT = `You are a support agent. Write a helpful response to th
2776
2745
  - "I understand" or "I hear you"
2777
2746
  - "Thanks for reaching out"
2778
2747
  - Em dashes (\u2014)
2748
+ - "Someone will reach out" / "I've escalated this"
2749
+ - "Pricing varies" (give the link instead)
2750
+
2751
+ ## Prices and Numbers
2752
+ - NEVER cite specific dollar amounts, percentages, or discount numbers unless they appear in the gathered context below
2753
+ - If you don't have pricing data, link to the pricing/buy page instead
2754
+ - Wrong: "The course is $495 with a 45% discount"
2755
+ - Right: "You can see current pricing at [URL]"
2779
2756
 
2780
2757
  ## If You Don't Have Info
2781
2758
  Don't make things up. If knowledge base has no answer:
@@ -2812,9 +2789,10 @@ function buildBillingPrompt(context) {
2812
2789
  return `${BASE_DRAFT_PROMPT}
2813
2790
 
2814
2791
  ## Billing/Invoice
2815
- - Point them to the invoices page: ${invoicesUrl}
2816
- - Invoices are customizable - they can add company/tax info
2817
- - PDFs are editable if they need adjustments`;
2792
+ - Give them the direct link: ${invoicesUrl}
2793
+ - That's it. Invoices are customizable (company name, tax ID, address) and PDFs are editable.
2794
+ - Don't write a numbered step-by-step guide \u2014 just the link + one sentence.
2795
+ - NEVER fabricate dollar amounts or discount percentages.`;
2818
2796
  }
2819
2797
  function buildPresalesFaqPrompt(context) {
2820
2798
  let promotionsSection = "";
@@ -2839,7 +2817,8 @@ Use this data to answer pricing, discount, and coupon questions. If a customer a
2839
2817
  ## Presales FAQ
2840
2818
  - Answer pricing, curriculum, requirements, and discount questions
2841
2819
  - Only reference information from the knowledge base or gathered context
2842
- - Don't fabricate pricing or feature details
2820
+ - NEVER fabricate specific dollar amounts or discount percentages
2821
+ - For pricing questions: link to the buy/pricing page, explain that PPP/regional pricing adjusts automatically at checkout
2843
2822
  - If unsure, offer to connect them with the team${promotionsSection}`;
2844
2823
  }
2845
2824
  function buildPresalesTeamPrompt(context) {
@@ -2870,17 +2849,16 @@ Use this data to answer team seat questions. If a customer needs more seats or w
2870
2849
  ## Team/Enterprise Inquiries
2871
2850
  - Answer questions about team licensing, seat management, enterprise pricing
2872
2851
  - If license data is available above, reference it directly
2873
- - For pricing questions beyond what's shown, offer to connect with the team
2874
- - For enterprise custom deals, escalate to the team${licenseSection}`;
2852
+ - Link to the buy/pricing page for pricing questions \u2014 don't say "someone will reach out" or "pricing varies"
2853
+ - NEVER fabricate seat counts, pricing tiers, or discount numbers${licenseSection}`;
2875
2854
  }
2876
2855
  var STATIC_CATEGORY_PROMPTS = {
2877
2856
  support_access: `${BASE_DRAFT_PROMPT}
2878
2857
 
2879
2858
  ## Access Issues
2880
- - First check if we found their purchase
2881
- - If no purchase found: ask which email they used to buy
2882
- - If purchase found: offer magic link or check their login method
2883
- - GitHub login issues: they may have multiple GitHub accounts`,
2859
+ - If purchase found: send the magic login link directly
2860
+ - If no purchase found: ask which email they used to buy (1 sentence)
2861
+ - Don't write a multi-step troubleshooting guide unless they've already tried the basics`,
2884
2862
  support_transfer: `${BASE_DRAFT_PROMPT}
2885
2863
 
2886
2864
  ## Transfer Requests
@@ -3148,6 +3126,34 @@ async function gather(input, options = {}) {
3148
3126
  })
3149
3127
  );
3150
3128
  }
3129
+ if (tools.searchResolvedThreads) {
3130
+ const query = `${message.subject} ${message.body}`.slice(0, 500);
3131
+ gatherPromises.push(
3132
+ withTimeout(
3133
+ (async () => {
3134
+ try {
3135
+ const resolvedThreads = await tools.searchResolvedThreads(
3136
+ query,
3137
+ appId
3138
+ );
3139
+ result.knowledge = [...result.knowledge, ...resolvedThreads];
3140
+ } catch (error) {
3141
+ result.gatherErrors.push({
3142
+ step: "resolved_threads",
3143
+ error: error instanceof Error ? error.message : "Unknown error"
3144
+ });
3145
+ }
3146
+ })(),
3147
+ timeout,
3148
+ "resolved threads search"
3149
+ ).catch((err) => {
3150
+ result.gatherErrors.push({
3151
+ step: "resolved_threads",
3152
+ error: err.message
3153
+ });
3154
+ })
3155
+ );
3156
+ }
3151
3157
  if (tools.getHistory && message.conversationId) {
3152
3158
  gatherPromises.push(
3153
3159
  withTimeout(
@@ -3560,7 +3566,7 @@ ${message.body}
3560
3566
  ---
3561
3567
  Write your response:`;
3562
3568
  if (useAgentMode && appId) {
3563
- const { runSupportAgent } = await import("./config-HHQF5U6R.js");
3569
+ const { runSupportAgent } = await import("./config-3GIHLVO4.js");
3564
3570
  await log("debug", "draft using agent mode", {
3565
3571
  workflow: "pipeline",
3566
3572
  step: "draft",
@@ -3670,6 +3676,25 @@ Write your response:`;
3670
3676
  });
3671
3677
  const toolsUsed = memories.length > 0 ? ["memory_query"] : [];
3672
3678
  const durationMs = Date.now() - startTime;
3679
+ if (!result.text?.trim()) {
3680
+ await log("warn", "draft:empty-response", {
3681
+ workflow: "pipeline",
3682
+ step: "draft",
3683
+ appId,
3684
+ conversationId,
3685
+ category: classification.category,
3686
+ model,
3687
+ durationMs
3688
+ });
3689
+ return {
3690
+ draft: "",
3691
+ reasoning: "LLM returned empty response",
3692
+ toolsUsed,
3693
+ durationMs,
3694
+ templateUsed: void 0,
3695
+ memoriesCited: void 0
3696
+ };
3697
+ }
3673
3698
  await log("info", "draft:decision", {
3674
3699
  workflow: "pipeline",
3675
3700
  step: "draft",
@@ -3841,7 +3866,8 @@ var ROUTING_RULES = [
3841
3866
  // NOTE: Must be BEFORE unknown_escalate so isPersonalToInstructor signal is honored
3842
3867
  {
3843
3868
  name: "personal_to_instructor",
3844
- condition: ({ classification }) => classification.signals.isPersonalToInstructor,
3869
+ condition: ({ classification }) => classification.signals.isPersonalToInstructor && // ADR-0003 D4: presales_faq should respond, not escalate to instructor
3870
+ classification.category !== "presales_faq",
3845
3871
  action: "escalate_instructor",
3846
3872
  reason: "Personal message addressed to instructor"
3847
3873
  },
@@ -4055,7 +4081,8 @@ var THREAD_ROUTING_RULES = [
4055
4081
  // NOTE: Must be BEFORE unknown_escalate so isPersonalToInstructor signal is honored
4056
4082
  {
4057
4083
  name: "personal_to_instructor",
4058
- condition: ({ classification }) => classification.signals.isPersonalToInstructor,
4084
+ condition: ({ classification }) => classification.signals.isPersonalToInstructor && // ADR-0003 D4: presales_faq should respond, not escalate to instructor
4085
+ classification.category !== "presales_faq",
4059
4086
  action: "escalate_instructor",
4060
4087
  reason: "Personal message addressed to instructor"
4061
4088
  },
@@ -4700,6 +4727,19 @@ var FABRICATION_PATTERNS = {
4700
4727
  percentage: /\d+%/g,
4701
4728
  guarantee: /guarantee|always|never|definitely|certainly/gi
4702
4729
  };
4730
+ var SIGNATURE_PATTERNS = [
4731
+ /\n\s*Best,?\s*$/im,
4732
+ /\n\s*Thanks,?\s*$/im,
4733
+ /\n\s*Cheers,?\s*$/im,
4734
+ /\n\s*Sincerely,?\s*$/im,
4735
+ /\n\s*Kind regards,?\s*$/im,
4736
+ /\n\s*Best regards,?\s*$/im,
4737
+ /\n\s*Warm regards,?\s*$/im,
4738
+ /\bTeam AI Hero\b/i,
4739
+ /\bTeam Total TypeScript\b/i,
4740
+ /\bTeam Epic Web\b/i,
4741
+ /\bTeam egghead\b/i
4742
+ ];
4703
4743
  var MIN_RESPONSE_LENGTH = 10;
4704
4744
  var MAX_RESPONSE_LENGTH = 2e3;
4705
4745
  function calculateConfidenceScore(issues) {
@@ -4767,6 +4807,23 @@ function checkBannedPhrases(draft2) {
4767
4807
  }
4768
4808
  return issues;
4769
4809
  }
4810
+ function checkSignatures(draft2) {
4811
+ const issues = [];
4812
+ for (const pattern of SIGNATURE_PATTERNS) {
4813
+ const match = draft2.match(pattern);
4814
+ if (match) {
4815
+ issues.push({
4816
+ type: "banned_phrase",
4817
+ severity: "error",
4818
+ message: "Response contains a signature (Front adds signatures automatically)",
4819
+ match: match[0].trim(),
4820
+ position: match.index
4821
+ });
4822
+ break;
4823
+ }
4824
+ }
4825
+ return issues;
4826
+ }
4770
4827
  function checkFabrication(draft2, context) {
4771
4828
  const issues = [];
4772
4829
  const hasKnowledge = (context.knowledge?.length ?? 0) > 0;
@@ -5059,6 +5116,7 @@ function validateSync(input) {
5059
5116
  ...checkInternalLeaks(draft2),
5060
5117
  ...checkMetaCommentary(draft2),
5061
5118
  ...checkBannedPhrases(draft2),
5119
+ ...checkSignatures(draft2),
5062
5120
  ...checkFabrication(draft2, context),
5063
5121
  ...checkLength(draft2)
5064
5122
  ];
@@ -5096,6 +5154,7 @@ async function validate(input, options = {}) {
5096
5154
  ...checkInternalLeaks(draft2),
5097
5155
  ...checkMetaCommentary(draft2),
5098
5156
  ...checkBannedPhrases(draft2),
5157
+ ...checkSignatures(draft2),
5099
5158
  ...checkFabrication(draft2, context),
5100
5159
  ...checkLength(draft2)
5101
5160
  ];
@@ -7096,4 +7155,4 @@ export {
7096
7155
  runPipeline,
7097
7156
  runThreadPipeline
7098
7157
  };
7099
- //# sourceMappingURL=chunk-F3WI3BN5.js.map
7158
+ //# sourceMappingURL=chunk-4VWYLYQ6.js.map