@launchsecure/launch-kit 0.0.34 → 0.0.36

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.
@@ -27210,6 +27210,9 @@ function buildAnalyzerMcpConfig() {
27210
27210
  });
27211
27211
  }
27212
27212
  function buildAnalyzerPrompt(ctx) {
27213
+ if (ctx.kind === "event") {
27214
+ return buildEventPrompt(ctx);
27215
+ }
27213
27216
  const lines = [];
27214
27217
  lines.push("New user feedback received via the LaunchSecure beacon. Investigate, fix, test, and commit locally. DO NOT push \u2014 local commits only.");
27215
27218
  lines.push("");
@@ -27307,6 +27310,55 @@ function buildAnalyzerPrompt(ctx) {
27307
27310
  lines.push(` ${n++}. If any fix is non-trivial, ambiguous, or would touch many files, STOP and either (a) ask via \`AskUserQuestion\`, or (b) write a plan in your final response instead of editing. Better to leave the user a clear next-step than to commit a wrong change.`);
27308
27311
  return lines.join("\n");
27309
27312
  }
27313
+ function buildEventPrompt(ctx) {
27314
+ const lines = [];
27315
+ const userPrompt = (ctx.rulePrompt ?? "").trim();
27316
+ lines.push(
27317
+ userPrompt || "A LaunchSecure event matched a radar capture rule. Investigate and act on it. Make local commits only \u2014 DO NOT push."
27318
+ );
27319
+ lines.push("");
27320
+ lines.push("OUTPUT FORMAT \u2014 MANDATORY. Your VERY FIRST line of output, before any tool calls or thinking, must be:");
27321
+ lines.push(" `<bug|feature|cosmetic|question|other>: <one short sentence>`");
27322
+ lines.push("");
27323
+ lines.push("CONTEXT \u2014 auto-injected from the event that triggered this rule. Treat it as ground truth for what happened; the instruction above is your task.");
27324
+ if (ctx.eventType) lines.push(`- Event: ${ctx.eventType}`);
27325
+ if (ctx.eventMessage) lines.push(`- Summary: ${ctx.eventMessage}`);
27326
+ const metaLines = formatMetadataLines(ctx.eventMetadata ?? {});
27327
+ if (metaLines.length > 0) {
27328
+ lines.push("- Event fields:");
27329
+ for (const l of metaLines) lines.push(` ${l}`);
27330
+ }
27331
+ if (ctx.enrichedEntity) {
27332
+ lines.push("");
27333
+ lines.push("ENTITY DETAILS (fetched fresh from LaunchSecure at analysis time):");
27334
+ lines.push("```json");
27335
+ lines.push(safeJson(ctx.enrichedEntity));
27336
+ lines.push("```");
27337
+ } else if (ctx.entityRef && ctx.entityRef.kind !== "none") {
27338
+ lines.push(`- Linked ${ctx.entityRef.kind}: ${ctx.entityRef.id ?? "(id unavailable)"} (not prefetched \u2014 fetch via MCP if you need its details)`);
27339
+ }
27340
+ lines.push("");
27341
+ lines.push("TOOL DISCIPLINE \u2014 use the launch-chart MCP as the DEFAULT for code investigation (read_graph / inspect_node / grep_nodes); fall back to Read/Grep/Glob only when chart can't answer.");
27342
+ lines.push("");
27343
+ lines.push("PROCEDURE: emit the category line, carry out the instruction grounded in the CONTEXT above, keep changes minimal and scoped, run any relevant tests, and commit locally with a conventional message. DO NOT push. If the task is ambiguous or wide-reaching, STOP and ask via AskUserQuestion or leave a plan instead of editing.");
27344
+ return lines.join("\n");
27345
+ }
27346
+ function formatMetadataLines(md) {
27347
+ const SKIP = /* @__PURE__ */ new Set(["resourceType", "resourceId", "resource"]);
27348
+ const out = [];
27349
+ for (const [k, v] of Object.entries(md)) {
27350
+ if (SKIP.has(k) || v === null || v === void 0 || typeof v === "object") continue;
27351
+ out.push(`${k}: ${String(v)}`);
27352
+ }
27353
+ return out;
27354
+ }
27355
+ function safeJson(v) {
27356
+ try {
27357
+ return JSON.stringify(v, null, 2).slice(0, 4e3);
27358
+ } catch {
27359
+ return String(v);
27360
+ }
27361
+ }
27310
27362
  function formatEventOffset(eventTs, capturedAt) {
27311
27363
  if (!capturedAt) return `ts=${eventTs}`;
27312
27364
  const diffMs = eventTs - new Date(capturedAt).getTime();
@@ -27568,6 +27620,131 @@ function parseBody(text) {
27568
27620
 
27569
27621
  // src/server/radar/receiver.ts
27570
27622
  var import_node_crypto5 = require("node:crypto");
27623
+
27624
+ // src/server/radar/rules.ts
27625
+ var WEBHOOK_LIFECYCLE_EVENTS = [
27626
+ "webhook.activated",
27627
+ "webhook.disabled",
27628
+ "webhook.deleted"
27629
+ ];
27630
+ var COMMENT_TYPES = ["COMMENT_CREATED", "COMMENT_REPLY"];
27631
+ var BUILTIN_FEEDBACK_RULE = {
27632
+ id: "builtin-feedback",
27633
+ name: "Beacon feedback",
27634
+ enabled: true,
27635
+ trigger: "COMMENT_CREATED",
27636
+ filters: [],
27637
+ // the feedback predicate is special-cased (OR across sources)
27638
+ enrich: false,
27639
+ // beacon already fat-packs the context
27640
+ prompt: ""
27641
+ // empty → analyze.ts uses the built-in feedback template
27642
+ };
27643
+ function resolveRules(stored) {
27644
+ if (Array.isArray(stored) && stored.length > 0) {
27645
+ return stored;
27646
+ }
27647
+ return [BUILTIN_FEEDBACK_RULE];
27648
+ }
27649
+ function subscribedEventTypes(rules) {
27650
+ const set = new Set(WEBHOOK_LIFECYCLE_EVENTS);
27651
+ for (const rule of rules) {
27652
+ if (!rule.enabled) {
27653
+ continue;
27654
+ }
27655
+ if (COMMENT_TYPES.includes(rule.trigger)) {
27656
+ for (const t of COMMENT_TYPES) {
27657
+ set.add(t);
27658
+ }
27659
+ } else {
27660
+ set.add(rule.trigger);
27661
+ }
27662
+ }
27663
+ return [...set];
27664
+ }
27665
+ function isFeedbackPayload(payload) {
27666
+ if (!COMMENT_TYPES.includes(payload.type)) {
27667
+ return false;
27668
+ }
27669
+ const md = payload.metadata ?? {};
27670
+ const tags = extractTags(md).map((t) => t.toLowerCase());
27671
+ if (tags.includes("feedback") || tags.includes("bug")) {
27672
+ return true;
27673
+ }
27674
+ const resource = md.resource ?? {};
27675
+ if (resource.linkedResourceType === "feedback") {
27676
+ return true;
27677
+ }
27678
+ if (md.linkedResourceType === "feedback") {
27679
+ return true;
27680
+ }
27681
+ return false;
27682
+ }
27683
+ function extractTags(metadata) {
27684
+ const t = metadata?.tags;
27685
+ if (Array.isArray(t)) {
27686
+ return t.filter((x) => typeof x === "string");
27687
+ }
27688
+ return [];
27689
+ }
27690
+ function metaValue(metadata, field) {
27691
+ if (field === "tags") {
27692
+ return extractTags(metadata);
27693
+ }
27694
+ return metadata[field];
27695
+ }
27696
+ function filterMatches(filter, metadata) {
27697
+ const actual = metaValue(metadata, filter.field);
27698
+ switch (filter.op) {
27699
+ case "eq":
27700
+ return typeof filter.value === "string" && String(actual) === filter.value;
27701
+ case "in": {
27702
+ const wanted = Array.isArray(filter.value) ? filter.value : [filter.value];
27703
+ if (Array.isArray(actual)) {
27704
+ return actual.some((a) => wanted.includes(String(a)));
27705
+ }
27706
+ return wanted.includes(String(actual));
27707
+ }
27708
+ case "contains": {
27709
+ const needle = Array.isArray(filter.value) ? filter.value[0] ?? "" : filter.value;
27710
+ if (Array.isArray(actual)) {
27711
+ return actual.map(String).includes(needle);
27712
+ }
27713
+ return typeof actual === "string" && actual.includes(needle);
27714
+ }
27715
+ default:
27716
+ return false;
27717
+ }
27718
+ }
27719
+ function triggerMatches(rule, type) {
27720
+ if (COMMENT_TYPES.includes(rule.trigger)) {
27721
+ return COMMENT_TYPES.includes(type);
27722
+ }
27723
+ return rule.trigger === type;
27724
+ }
27725
+ function matchRule(payload, rules) {
27726
+ const md = payload.metadata ?? {};
27727
+ for (const rule of rules) {
27728
+ if (!rule.enabled) {
27729
+ continue;
27730
+ }
27731
+ if (!triggerMatches(rule, payload.type)) {
27732
+ continue;
27733
+ }
27734
+ if (rule.id === BUILTIN_FEEDBACK_RULE.id) {
27735
+ if (isFeedbackPayload(payload)) {
27736
+ return rule;
27737
+ }
27738
+ continue;
27739
+ }
27740
+ if (rule.filters.every((f) => filterMatches(f, md))) {
27741
+ return rule;
27742
+ }
27743
+ }
27744
+ return null;
27745
+ }
27746
+
27747
+ // src/server/radar/receiver.ts
27571
27748
  var SIGNATURE_HEADER = "x-ls-signature";
27572
27749
  var EVENT_HEADER = "x-ls-event";
27573
27750
  var DELIVERY_HEADER = "x-ls-delivery";
@@ -27590,23 +27767,22 @@ function verifySignature(secret, signatureHeader, body) {
27590
27767
  if (a.length !== b.length) return false;
27591
27768
  return (0, import_node_crypto5.timingSafeEqual)(a, b);
27592
27769
  }
27593
- function extractTags(metadata) {
27594
- const t = metadata?.tags;
27595
- if (Array.isArray(t)) return t.filter((x) => typeof x === "string");
27596
- return [];
27597
- }
27598
- function isFeedbackEvent(payload) {
27599
- if (payload.type !== "COMMENT_CREATED" && payload.type !== "COMMENT_REPLY") return false;
27770
+ function buildEventContext(payload, rule) {
27600
27771
  const md = payload.metadata ?? {};
27601
- const tags = extractTags(md).map((t) => t.toLowerCase());
27602
- if (tags.includes("feedback") || tags.includes("bug")) return true;
27603
- const resource = md.resource ?? {};
27604
- if (resource.linkedResourceType === "feedback") return true;
27605
- if (md.linkedResourceType === "feedback") return true;
27606
- return false;
27772
+ const isWorkItem = payload.type.startsWith("work_item.");
27773
+ const workItemId = typeof md.workItemId === "string" ? md.workItemId : typeof md.resourceId === "string" ? md.resourceId : void 0;
27774
+ return {
27775
+ kind: "event",
27776
+ eventType: payload.type,
27777
+ eventMessage: payload.message,
27778
+ eventMetadata: md,
27779
+ rulePrompt: rule.prompt,
27780
+ entityRef: rule.enrich && isWorkItem ? { kind: "work_item", id: workItemId } : { kind: "none" },
27781
+ body: payload.message
27782
+ };
27607
27783
  }
27608
27784
  function createReceiver(params) {
27609
- const { state, callbacks } = params;
27785
+ const { state, callbacks, getRules } = params;
27610
27786
  return (req, res) => {
27611
27787
  const chunks = [];
27612
27788
  req.on("data", (c) => chunks.push(c));
@@ -27636,7 +27812,7 @@ function createReceiver(params) {
27636
27812
  res.end(JSON.stringify({ ok: true }));
27637
27813
  const eventHeader = req.headers[EVENT_HEADER] ?? payload.type;
27638
27814
  const deliveryId = req.headers[DELIVERY_HEADER] ?? "?";
27639
- handlePayload(payload, eventHeader, deliveryId, state, callbacks);
27815
+ handlePayload(payload, eventHeader, deliveryId, state, callbacks, getRules);
27640
27816
  });
27641
27817
  req.on("error", () => {
27642
27818
  try {
@@ -27647,7 +27823,7 @@ function createReceiver(params) {
27647
27823
  });
27648
27824
  };
27649
27825
  }
27650
- function handlePayload(payload, eventType, deliveryId, state, cb) {
27826
+ function handlePayload(payload, eventType, deliveryId, state, cb, getRules) {
27651
27827
  switch (eventType) {
27652
27828
  case "webhook.activated":
27653
27829
  console.log(`[radar] \u25C9 webhook.activated (${deliveryId})`);
@@ -27661,15 +27837,16 @@ function handlePayload(payload, eventType, deliveryId, state, cb) {
27661
27837
  console.log(`[radar] \u2717 webhook.deleted (${deliveryId}) \u2014 re-registering`);
27662
27838
  cb.onDeleted();
27663
27839
  return;
27664
- default:
27665
- if (!isFeedbackEvent(payload)) {
27840
+ default: {
27841
+ const rule = matchRule(payload, getRules());
27842
+ if (!rule) {
27666
27843
  return;
27667
27844
  }
27668
27845
  if (state.hasSeen(payload.id)) {
27669
27846
  console.log(`[radar] dup ${payload.id} \u2014 skipping`);
27670
27847
  return;
27671
27848
  }
27672
- const context = buildContext(payload);
27849
+ const context = rule.id === BUILTIN_FEEDBACK_RULE.id ? buildContext(payload) : buildEventContext(payload, rule);
27673
27850
  const threadId = extractThreadId(payload);
27674
27851
  const ping = {
27675
27852
  id: payload.id,
@@ -27677,14 +27854,17 @@ function handlePayload(payload, eventType, deliveryId, state, cb) {
27677
27854
  receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
27678
27855
  threadId,
27679
27856
  context,
27680
- state: "queued"
27857
+ state: "queued",
27681
27858
  // analyzer flips to 'analyzing' on pickup
27859
+ ruleId: rule.id,
27860
+ ruleName: rule.name
27682
27861
  };
27683
27862
  const appended = state.appendPingIfNew(ping);
27684
27863
  if (appended) {
27685
- console.log(`[radar] \u25C9 ping: "${ping.title}" (${deliveryId})`);
27864
+ console.log(`[radar] \u25C9 ping [${rule.name}]: "${ping.title}" (${deliveryId})`);
27686
27865
  cb.onPing(ping);
27687
27866
  }
27867
+ }
27688
27868
  }
27689
27869
  }
27690
27870
  function extractThreadId(payload) {
@@ -28043,6 +28223,12 @@ var RadarState = class {
28043
28223
  p.context.screenshotLocalPath = path13;
28044
28224
  });
28045
28225
  }
28226
+ /** Persist entity details fetched (via MCP) for a rule with enrich=true. */
28227
+ setEnrichedEntity(id, entity) {
28228
+ return this.mutate(id, (p) => {
28229
+ p.context.enrichedEntity = entity;
28230
+ });
28231
+ }
28046
28232
  /**
28047
28233
  * Wipe all analysis-result fields so the ping can be re-enqueued from a
28048
28234
  * clean state. Preserves immutable identity fields (id, threadId, title,
@@ -28678,6 +28864,8 @@ var Radar = class _Radar {
28678
28864
  }
28679
28865
  this.tunnel = tunnel;
28680
28866
  this.maxConcurrent = parsePositiveInt(process.env.RADAR_MAX_CONCURRENT_ANALYSES);
28867
+ this.rules = resolveRules(opts.rules);
28868
+ console.log(`[radar] ${this.rules.length} capture rule(s): ${this.rules.map((r) => r.name).join(", ")}`);
28681
28869
  const callbacks = {
28682
28870
  onLive: () => this.handleLive(),
28683
28871
  onDeactivated: () => this.handleDeactivated(),
@@ -28686,7 +28874,7 @@ var Radar = class _Radar {
28686
28874
  },
28687
28875
  onPing: (ping) => this.handlePing(ping)
28688
28876
  };
28689
- this.receiver = createReceiver({ state: this.state, callbacks });
28877
+ this.receiver = createReceiver({ state: this.state, callbacks, getRules: () => this.rules });
28690
28878
  }
28691
28879
  static {
28692
28880
  // Tunnel provider is hardcoded for now — radar today cannot function
@@ -28772,7 +28960,8 @@ var Radar = class _Radar {
28772
28960
  const result = await registerOrRefresh({
28773
28961
  mcp: this.mcp,
28774
28962
  state: this.state,
28775
- tunnelUrl: publicUrl
28963
+ tunnelUrl: publicUrl,
28964
+ eventTypes: subscribedEventTypes(this.rules)
28776
28965
  });
28777
28966
  if (result.approvalStatus === "approved") {
28778
28967
  this.status = "live";
@@ -28862,6 +29051,21 @@ var Radar = class _Radar {
28862
29051
  }
28863
29052
  }
28864
29053
  }
29054
+ const ref = ping.context.entityRef;
29055
+ if (ref && ref.kind === "work_item" && ref.id) {
29056
+ try {
29057
+ const item = await this.mcp.call("work_item_get", { item_id: ref.id });
29058
+ if (item && typeof item === "object") {
29059
+ const u = this.state.setEnrichedEntity(ping.id, item);
29060
+ if (u) {
29061
+ ping = u;
29062
+ this.broadcastPingState(u);
29063
+ }
29064
+ }
29065
+ } catch (err2) {
29066
+ console.warn(`[radar] enrichment fetch failed for ${ping.id}: ${err2 instanceof Error ? err2.message : String(err2)}`);
29067
+ }
29068
+ }
28865
29069
  const { sessionId } = await spawnAnalysisSession({ ping, projectDir: this.opts.projectRoot });
28866
29070
  const updated = this.state.setAnalysisStarted(ping.id, sessionId);
28867
29071
  console.log(`[radar] \u25B6 analyzing ${ping.id} \u2192 session ${sessionId.slice(-8)}`);
@@ -36689,6 +36893,7 @@ function parseArgs() {
36689
36893
  const args = process.argv.slice(2);
36690
36894
  let port = 0;
36691
36895
  let token = null;
36896
+ let course = null;
36692
36897
  const subcommand = args[0] && !args[0].startsWith("--") ? args[0] : null;
36693
36898
  for (let i = 0; i < args.length; i++) {
36694
36899
  if (args[i].startsWith("--token=")) {
@@ -36699,6 +36904,10 @@ function parseArgs() {
36699
36904
  port = parseInt(args[i].slice("--port=".length), 10);
36700
36905
  } else if (args[i] === "--port" && args[i + 1]) {
36701
36906
  port = parseInt(args[++i], 10);
36907
+ } else if (args[i].startsWith("--course=")) {
36908
+ course = args[i].slice("--course=".length);
36909
+ } else if (args[i] === "--course" && args[i + 1]) {
36910
+ course = args[++i];
36702
36911
  }
36703
36912
  }
36704
36913
  if (!port) {
@@ -36715,7 +36924,7 @@ function parseArgs() {
36715
36924
  }
36716
36925
  }
36717
36926
  if (!port) port = 52718;
36718
- return { port, token, serverUrl: LAUNCHSECURE_URL, subcommand };
36927
+ return { port, token, serverUrl: LAUNCHSECURE_URL, subcommand, course };
36719
36928
  }
36720
36929
  function tryListen(server, port, maxRetries = 10) {
36721
36930
  return new Promise((resolve6, reject) => {
@@ -37286,7 +37495,7 @@ if (parsedArgs.subcommand === "mcp:graph") {
37286
37495
  var __isMcpMode = parsedArgs.subcommand === "mcp:graph";
37287
37496
  var __isRadarMode = parsedArgs.subcommand === "radar";
37288
37497
  var radar = null;
37289
- function readLaunchSecureMcpConfig() {
37498
+ function readLaunchSecureMcpConfig(courseOverride) {
37290
37499
  const fix = `Run \`npx @launchsecure/launch-kit@latest refresh\` to re-sync this project (or \`init\` if you haven't bootstrapped yet).`;
37291
37500
  const cred = readCredFile(REPO_ROOT);
37292
37501
  if (!cred) {
@@ -37296,9 +37505,12 @@ function readLaunchSecureMcpConfig() {
37296
37505
  if (!nested) {
37297
37506
  throw new Error(`${CONFIG_FILENAME} is malformed or missing required fields (pat/orgSlug/projectSlug/serverUrl). ${fix}`);
37298
37507
  }
37299
- const profile = nested.profiles[nested.active];
37508
+ const courseName = courseOverride || nested.active;
37509
+ const profile = nested.profiles[courseName];
37300
37510
  if (!profile) {
37301
- throw new Error(`${CONFIG_FILENAME} active course "${nested.active}" not found in profiles. ${fix}`);
37511
+ const available = Object.keys(nested.profiles).join(", ") || "(none)";
37512
+ const why = courseOverride ? `course "${courseName}" (from --course) not found in profiles. Available: ${available}.` : `active course "${courseName}" not found in profiles. ${fix}`;
37513
+ throw new Error(`${CONFIG_FILENAME} ${why}`);
37302
37514
  }
37303
37515
  const { pat, orgSlug, projectSlug, serverUrl } = profile;
37304
37516
  if (!pat || !pat.startsWith("ls_pat_")) {
@@ -37307,7 +37519,7 @@ function readLaunchSecureMcpConfig() {
37307
37519
  if (!orgSlug || !projectSlug || !serverUrl) {
37308
37520
  throw new Error(`${CONFIG_FILENAME} is missing required fields (orgSlug, projectSlug, serverUrl). ${fix}`);
37309
37521
  }
37310
- return { pat, orgSlug, projectSlug, serverUrl, source: `${CONFIG_FILENAME}[${nested.active}]` };
37522
+ return { pat, orgSlug, projectSlug, serverUrl, source: `${CONFIG_FILENAME}[${courseName}]`, course: courseName };
37311
37523
  }
37312
37524
  if (!__isMcpMode) {
37313
37525
  let gracefulShutdown = function() {
@@ -37728,7 +37940,7 @@ if (!__isMcpMode) {
37728
37940
  if (__isRadarMode) {
37729
37941
  let cfg;
37730
37942
  try {
37731
- cfg = readLaunchSecureMcpConfig();
37943
+ cfg = readLaunchSecureMcpConfig(parsedArgs.course);
37732
37944
  } catch (err2) {
37733
37945
  console.error(`[radar] ${err2 instanceof Error ? err2.message : String(err2)}`);
37734
37946
  process.exit(1);
@@ -37736,9 +37948,40 @@ if (!__isMcpMode) {
37736
37948
  const actualPort2 = await tryListen(server, PORT);
37737
37949
  PORT = actualPort2;
37738
37950
  const localUrl = `http://127.0.0.1:${PORT}`;
37739
- console.log(`LaunchPod radar \xB7 ${localUrl}`);
37740
- console.log(` cloud: ${cfg.serverUrl} \xB7 source: ${cfg.source}`);
37741
- console.log(` org: ${cfg.orgSlug} \xB7 project: ${cfg.projectSlug}`);
37951
+ const dim = (s) => `\x1B[2m${s}\x1B[0m`;
37952
+ const bold = (s) => `\x1B[1m${s}\x1B[0m`;
37953
+ const cyan = (s) => `\x1B[36m${s}\x1B[0m`;
37954
+ const COURSE_LABELS = {
37955
+ prod: "Production",
37956
+ production: "Production",
37957
+ staging: "Staging",
37958
+ stage: "Staging",
37959
+ local: "Local",
37960
+ dev: "Development",
37961
+ development: "Development"
37962
+ };
37963
+ const courseLabel = COURSE_LABELS[cfg.course.toLowerCase()] ?? cfg.course.charAt(0).toUpperCase() + cfg.course.slice(1);
37964
+ const row = (label, value) => ` ${dim(label.padEnd(11))}${value}`;
37965
+ console.log("");
37966
+ console.log(` ${cyan("\u25C9")} ${bold("LaunchPod Radar")}`);
37967
+ console.log("");
37968
+ const courseTail = parsedArgs.course ? dim(`\xB7 ${cfg.course} \xB7 --course override`) : dim(`\xB7 ${cfg.course}`);
37969
+ console.log(row("Course", `${bold(courseLabel)} ${courseTail}`));
37970
+ console.log(row("Cloud", cfg.serverUrl));
37971
+ console.log(row("Project", `${cfg.orgSlug} ${dim("/")} ${cfg.projectSlug}`));
37972
+ console.log(row("Active on", localUrl));
37973
+ console.log("");
37974
+ let radarRules = null;
37975
+ if (process.env.RADAR_RULES) {
37976
+ try {
37977
+ const parsed = JSON.parse(process.env.RADAR_RULES);
37978
+ if (Array.isArray(parsed)) {
37979
+ radarRules = parsed;
37980
+ }
37981
+ } catch {
37982
+ console.warn("[radar] RADAR_RULES is not valid JSON \u2014 using the built-in feedback rule");
37983
+ }
37984
+ }
37742
37985
  try {
37743
37986
  radar = new Radar({
37744
37987
  projectRoot: PROJECT_DIR,
@@ -37747,7 +37990,8 @@ if (!__isMcpMode) {
37747
37990
  serverUrl: cfg.serverUrl,
37748
37991
  pat: cfg.pat,
37749
37992
  orgSlug: cfg.orgSlug,
37750
- projectSlug: cfg.projectSlug
37993
+ projectSlug: cfg.projectSlug,
37994
+ rules: radarRules
37751
37995
  });
37752
37996
  } catch (err2) {
37753
37997
  console.error(`${err2 instanceof Error ? err2.message : String(err2)}`);