robrain 2.4.11 → 2.5.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.
@@ -7190,16 +7190,179 @@ var require_trust = __commonJS({
7190
7190
  }
7191
7191
  });
7192
7192
 
7193
+ // ../shared/dist/capture-settings.js
7194
+ var require_capture_settings = __commonJS({
7195
+ "../shared/dist/capture-settings.js"(exports) {
7196
+ "use strict";
7197
+ Object.defineProperty(exports, "__esModule", { value: true });
7198
+ exports.DEFAULT_CAPTURE_SETTINGS = exports.DEFAULT_CAPTURE_KIND = exports.CAPTURE_MODES = exports.CAPTURE_KIND_DESCRIPTION_MAX_CHARS = exports.CAPTURE_KIND_NAME_MAX_CHARS = exports.CAPTURE_KINDS_MAX = exports.CAPTURE_INSTRUCTIONS_MAX_CHARS = void 0;
7199
+ exports.isDefaultCaptureSettings = isDefaultCaptureSettings;
7200
+ exports.normalizeKindName = normalizeKindName;
7201
+ exports.applyCaptureSettingsPatch = applyCaptureSettingsPatch;
7202
+ exports.coerceCaptureSettings = coerceCaptureSettings2;
7203
+ exports.subjectTokens = subjectTokens;
7204
+ exports.subjectsMatch = subjectsMatch;
7205
+ exports.effectiveConfidenceMin = effectiveConfidenceMin2;
7206
+ exports.allowedKindNames = allowedKindNames;
7207
+ exports.CAPTURE_INSTRUCTIONS_MAX_CHARS = 2e3;
7208
+ exports.CAPTURE_KINDS_MAX = 8;
7209
+ exports.CAPTURE_KIND_NAME_MAX_CHARS = 32;
7210
+ exports.CAPTURE_KIND_DESCRIPTION_MAX_CHARS = 300;
7211
+ exports.CAPTURE_MODES = ["strict", "broad"];
7212
+ exports.DEFAULT_CAPTURE_KIND = "decision";
7213
+ exports.DEFAULT_CAPTURE_SETTINGS = {
7214
+ instructions: null,
7215
+ mode: "strict",
7216
+ confidence_min: null,
7217
+ kinds: []
7218
+ };
7219
+ function isDefaultCaptureSettings(s) {
7220
+ return s.instructions === null && s.mode === "strict" && s.confidence_min === null && s.kinds.length === 0;
7221
+ }
7222
+ function normalizeKindName(name) {
7223
+ return name.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
7224
+ }
7225
+ function applyCaptureSettingsPatch(current, patch) {
7226
+ const errors = [];
7227
+ const next = { ...current, kinds: [...current.kinds] };
7228
+ if (patch.instructions !== void 0) {
7229
+ if (patch.instructions === null || patch.instructions.trim() === "") {
7230
+ next.instructions = null;
7231
+ } else if (typeof patch.instructions !== "string") {
7232
+ errors.push("instructions must be text");
7233
+ } else if (patch.instructions.length > exports.CAPTURE_INSTRUCTIONS_MAX_CHARS) {
7234
+ errors.push(`instructions must be at most ${exports.CAPTURE_INSTRUCTIONS_MAX_CHARS} characters (got ${patch.instructions.length})`);
7235
+ } else {
7236
+ next.instructions = patch.instructions.trim();
7237
+ }
7238
+ }
7239
+ if (patch.mode !== void 0) {
7240
+ const mode = String(patch.mode).trim().toLowerCase();
7241
+ if (exports.CAPTURE_MODES.includes(mode)) {
7242
+ next.mode = mode;
7243
+ } else {
7244
+ errors.push(`mode must be one of: ${exports.CAPTURE_MODES.join(", ")}`);
7245
+ }
7246
+ }
7247
+ if (patch.confidence_min !== void 0) {
7248
+ if (patch.confidence_min === null) {
7249
+ next.confidence_min = null;
7250
+ } else if (typeof patch.confidence_min !== "number" || !Number.isFinite(patch.confidence_min)) {
7251
+ errors.push("confidence_min must be a number between 0 and 1");
7252
+ } else if (patch.confidence_min < 0 || patch.confidence_min > 1) {
7253
+ errors.push("confidence_min must be between 0 and 1");
7254
+ } else {
7255
+ next.confidence_min = Math.round(patch.confidence_min * 100) / 100;
7256
+ }
7257
+ }
7258
+ if (patch.kinds !== void 0) {
7259
+ if (patch.kinds === null) {
7260
+ next.kinds = [];
7261
+ } else if (!Array.isArray(patch.kinds)) {
7262
+ errors.push("kinds must be a list");
7263
+ } else if (patch.kinds.length > exports.CAPTURE_KINDS_MAX) {
7264
+ errors.push(`at most ${exports.CAPTURE_KINDS_MAX} kinds`);
7265
+ } else {
7266
+ const seen = /* @__PURE__ */ new Set();
7267
+ const kinds = [];
7268
+ for (const raw of patch.kinds) {
7269
+ const name = typeof raw?.name === "string" ? normalizeKindName(raw.name) : "";
7270
+ const description = typeof raw?.description === "string" ? raw.description.trim() : "";
7271
+ if (!name) {
7272
+ errors.push("each kind needs a name (letters, digits, - or _)");
7273
+ continue;
7274
+ }
7275
+ if (name.length > exports.CAPTURE_KIND_NAME_MAX_CHARS) {
7276
+ errors.push(`kind name "${name}" is longer than ${exports.CAPTURE_KIND_NAME_MAX_CHARS} characters`);
7277
+ continue;
7278
+ }
7279
+ if (name === exports.DEFAULT_CAPTURE_KIND) {
7280
+ errors.push(`"${exports.DEFAULT_CAPTURE_KIND}" is the built-in kind and cannot be redefined`);
7281
+ continue;
7282
+ }
7283
+ if (!description) {
7284
+ errors.push(`kind "${name}" needs a description so the model knows what to store`);
7285
+ continue;
7286
+ }
7287
+ if (description.length > exports.CAPTURE_KIND_DESCRIPTION_MAX_CHARS) {
7288
+ errors.push(`description for kind "${name}" is longer than ${exports.CAPTURE_KIND_DESCRIPTION_MAX_CHARS} characters`);
7289
+ continue;
7290
+ }
7291
+ if (seen.has(name)) {
7292
+ errors.push(`kind "${name}" is listed twice`);
7293
+ continue;
7294
+ }
7295
+ seen.add(name);
7296
+ kinds.push({ name, description });
7297
+ }
7298
+ next.kinds = kinds;
7299
+ }
7300
+ }
7301
+ return errors.length ? { ok: false, errors } : { ok: true, settings: next };
7302
+ }
7303
+ function coerceCaptureSettings2(raw) {
7304
+ if (!raw || typeof raw !== "object")
7305
+ return { ...exports.DEFAULT_CAPTURE_SETTINGS };
7306
+ const r = raw;
7307
+ const result = applyCaptureSettingsPatch(exports.DEFAULT_CAPTURE_SETTINGS, {
7308
+ instructions: typeof r.instructions === "string" ? r.instructions : null,
7309
+ mode: typeof r.mode === "string" ? r.mode : "strict",
7310
+ confidence_min: typeof r.confidence_min === "number" ? r.confidence_min : null,
7311
+ kinds: Array.isArray(r.kinds) ? r.kinds : []
7312
+ });
7313
+ if (result.ok)
7314
+ return result.settings;
7315
+ const safe = { ...exports.DEFAULT_CAPTURE_SETTINGS };
7316
+ const instructions = applyCaptureSettingsPatch(safe, { instructions: typeof r.instructions === "string" ? r.instructions : null });
7317
+ if (instructions.ok)
7318
+ safe.instructions = instructions.settings.instructions;
7319
+ const mode = applyCaptureSettingsPatch(safe, { mode: typeof r.mode === "string" ? r.mode : "strict" });
7320
+ if (mode.ok)
7321
+ safe.mode = mode.settings.mode;
7322
+ const conf = applyCaptureSettingsPatch(safe, { confidence_min: typeof r.confidence_min === "number" ? r.confidence_min : null });
7323
+ if (conf.ok)
7324
+ safe.confidence_min = conf.settings.confidence_min;
7325
+ const kinds = applyCaptureSettingsPatch(safe, { kinds: Array.isArray(r.kinds) ? r.kinds : [] });
7326
+ if (kinds.ok)
7327
+ safe.kinds = kinds.settings.kinds;
7328
+ return safe;
7329
+ }
7330
+ var SUBJECT_STOPWORDS = /* @__PURE__ */ new Set(["the", "a", "an", "of", "for", "on", "in", "at", "to", "and", "this", "that", "our", "my", "its", "last", "latest", "current"]);
7331
+ function subjectTokens(subject) {
7332
+ return subject.toLowerCase().replace(/[^a-z0-9\s_-]+/g, " ").split(/[\s_-]+/).filter((t) => t.length > 1 && !SUBJECT_STOPWORDS.has(t));
7333
+ }
7334
+ function subjectsMatch(a, b) {
7335
+ const ta = new Set(subjectTokens(a));
7336
+ const tb = new Set(subjectTokens(b));
7337
+ if (ta.size === 0 || tb.size === 0)
7338
+ return false;
7339
+ const [small, large] = ta.size <= tb.size ? [ta, tb] : [tb, ta];
7340
+ for (const t of small)
7341
+ if (!large.has(t))
7342
+ return false;
7343
+ return true;
7344
+ }
7345
+ function effectiveConfidenceMin2(settings, stock) {
7346
+ return settings?.confidence_min ?? stock;
7347
+ }
7348
+ function allowedKindNames(settings) {
7349
+ return [exports.DEFAULT_CAPTURE_KIND, ...(settings?.kinds ?? []).map((k) => k.name)];
7350
+ }
7351
+ }
7352
+ });
7353
+
7193
7354
  // ../shared/dist/extract-decision.js
7194
7355
  var require_extract_decision = __commonJS({
7195
7356
  "../shared/dist/extract-decision.js"(exports) {
7196
7357
  "use strict";
7197
7358
  Object.defineProperty(exports, "__esModule", { value: true });
7198
7359
  exports.LlmKeyMissingError = exports.SELF_HOSTED_USER_RULE_OVERRIDE = exports.DECISION_EXTRACTION_SYSTEM_PROMPT = void 0;
7360
+ exports.buildCaptureRules = buildCaptureRules;
7199
7361
  exports.stripMarkdownJsonFence = stripMarkdownJsonFence;
7200
7362
  exports.parseExtractedDecision = parseExtractedDecision;
7201
7363
  exports.extractDecisionLlm = extractDecisionLlm2;
7202
7364
  var llm_js_1 = require_llm();
7365
+ var capture_settings_js_1 = require_capture_settings();
7203
7366
  exports.DECISION_EXTRACTION_SYSTEM_PROMPT = `You extract technical decisions from software development conversations.
7204
7367
 
7205
7368
  A decision includes ANY of the following (not only formal deliberation):
@@ -7225,6 +7388,23 @@ Fields:
7225
7388
  Output ONLY valid JSON. If no decision: {"decision": null, "rationale": null, "rejected": [], "confidence": 0}.
7226
7389
  Never add explanation outside the JSON.
7227
7390
  Schema: {"decision": string|null, "rationale": string|null, "rejected": [{"option": string, "reason": string}], "confidence": number}`;
7391
+ function buildCaptureRules(capture) {
7392
+ if (!capture)
7393
+ return "";
7394
+ const sections = [];
7395
+ if (capture.kinds.length > 0) {
7396
+ const kindLines = capture.kinds.map((k) => `- "${k.name}": ${k.description}`).join("\n");
7397
+ sections.push(`Besides decisions, this project also stores these kinds of memory:
7398
+ ${kindLines}
7399
+ The rules above about what is NOT a decision apply only to kind "${capture_settings_js_1.DEFAULT_CAPTURE_KIND}". A turn that contains an entry of one of the listed kinds must be returned even though it contains no decision and nothing durable was committed: set "kind" to that kind's name, put the entry in "entry" as one short factual sentence (what happened, when, with what result), set "subject" to a short stable label for the thing it is about (max 6 words, the same words each time so a newer entry about the same subject replaces the older one), set "confidence" to how sure you are that the turn contains such an entry, and leave "decision" null. When the turn contains a decision, return it as usual with "kind": "${capture_settings_js_1.DEFAULT_CAPTURE_KIND}", "entry": null and "subject": null. Never invent a kind that is not listed.
7400
+ Schema: {"decision": string|null, "rationale": string|null, "rejected": [{"option": string, "reason": string}], "confidence": number, "kind": string, "entry": string|null, "subject": string|null}`);
7401
+ }
7402
+ if (capture.instructions) {
7403
+ sections.push(`Project instructions for what to store (they refine the rules above; the output must stay valid JSON in the schema given):
7404
+ ${capture.instructions}`);
7405
+ }
7406
+ return sections.join("\n\n");
7407
+ }
7228
7408
  exports.SELF_HOSTED_USER_RULE_OVERRIDE = `User-stated rule wins:
7229
7409
  - A durable rule is a standing instruction for future work in this repo or team (e.g. "from now on", "always", "never", "we no longer"). An instruction scoped to the current task, file, or step (e.g. "don't use that file", "stop logging here", "don't add tests for this one") is not durable; apply the rules above to it.
7230
7410
  - If the user states a durable rule in this turn, extract that rule even when the assistant refuses it, cites an older rule, or says it will not follow the new one. In that case do not return null, and do not extract the older rule the assistant restates.`;
@@ -7239,24 +7419,28 @@ Schema: {"decision": string|null, "rationale": string|null, "rejected": [{"optio
7239
7419
  function stripMarkdownJsonFence(raw) {
7240
7420
  return raw.trim().replace(/^```[a-z]*\s*/i, "").replace(/\s*```\s*$/, "").trim();
7241
7421
  }
7242
- function parseExtractedDecision(rawText) {
7422
+ function parseExtractedDecision(rawText, allowedKinds = [capture_settings_js_1.DEFAULT_CAPTURE_KIND]) {
7243
7423
  const raw = JSON.parse(stripMarkdownJsonFence(rawText));
7244
7424
  const rejected = Array.isArray(raw.rejected) ? raw.rejected.filter((x) => x !== null && typeof x === "object" && typeof x.option === "string" && typeof x.reason === "string") : [];
7245
7425
  const confidence = typeof raw.confidence === "number" && Number.isFinite(raw.confidence) ? raw.confidence : 0;
7426
+ const requestedKind = typeof raw.kind === "string" ? (0, capture_settings_js_1.normalizeKindName)(raw.kind) : capture_settings_js_1.DEFAULT_CAPTURE_KIND;
7427
+ const kind = allowedKinds.includes(requestedKind) ? requestedKind : capture_settings_js_1.DEFAULT_CAPTURE_KIND;
7428
+ const subject = kind !== capture_settings_js_1.DEFAULT_CAPTURE_KIND && typeof raw.subject === "string" && raw.subject.trim() ? raw.subject.trim().slice(0, 120) : null;
7429
+ const entry = kind !== capture_settings_js_1.DEFAULT_CAPTURE_KIND && typeof raw.entry === "string" && raw.entry.trim() ? raw.entry.trim() : null;
7246
7430
  return {
7247
- decision: typeof raw.decision === "string" ? raw.decision : null,
7431
+ decision: entry ?? (typeof raw.decision === "string" ? raw.decision : null),
7248
7432
  rationale: typeof raw.rationale === "string" ? raw.rationale : null,
7249
7433
  rejected,
7250
- confidence
7434
+ confidence,
7435
+ kind,
7436
+ subject
7251
7437
  };
7252
7438
  }
7253
7439
  async function extractDecisionLlm2(userMessage, claudeReply, cfg) {
7254
7440
  const userPrompt = `Session turn:
7255
7441
  User: ${userMessage}
7256
7442
  Claude: ${claudeReply}`;
7257
- const systemPrompt = cfg.extraSystemRules ? `${exports.DECISION_EXTRACTION_SYSTEM_PROMPT}
7258
-
7259
- ${cfg.extraSystemRules}` : exports.DECISION_EXTRACTION_SYSTEM_PROMPT;
7443
+ const systemPrompt = [exports.DECISION_EXTRACTION_SYSTEM_PROMPT, cfg.extraSystemRules, buildCaptureRules(cfg.capture)].filter((section) => Boolean(section)).join("\n\n");
7260
7444
  let rawText;
7261
7445
  if (cfg.provider === "openai") {
7262
7446
  const baseUrl = cfg.openaiBaseUrl ?? (0, llm_js_1.resolveOpenAiBaseUrl)();
@@ -7288,7 +7472,88 @@ ${cfg.extraSystemRules}` : exports.DECISION_EXTRACTION_SYSTEM_PROMPT;
7288
7472
  temperature: 0
7289
7473
  });
7290
7474
  }
7291
- return parseExtractedDecision(rawText);
7475
+ return parseExtractedDecision(rawText, (0, capture_settings_js_1.allowedKindNames)(cfg.capture));
7476
+ }
7477
+ }
7478
+ });
7479
+
7480
+ // ../shared/dist/capture-gate.js
7481
+ var require_capture_gate = __commonJS({
7482
+ "../shared/dist/capture-gate.js"(exports) {
7483
+ "use strict";
7484
+ Object.defineProperty(exports, "__esModule", { value: true });
7485
+ exports.BROAD_MODE_MIN_USER_CHARS = exports.DECISION_KEYWORDS = void 0;
7486
+ exports.captureGate = captureGate2;
7487
+ exports.DECISION_KEYWORDS = [
7488
+ "decided",
7489
+ "decision",
7490
+ "going with",
7491
+ "we'll use",
7492
+ "let's use",
7493
+ "instead of",
7494
+ "switched to",
7495
+ "switching to",
7496
+ "rejected",
7497
+ "won't use",
7498
+ "we tried",
7499
+ "too slow",
7500
+ "doesn't work",
7501
+ "went with",
7502
+ "chosen",
7503
+ "settled on",
7504
+ "ruling out",
7505
+ "dropping",
7506
+ "replacing",
7507
+ // Casual commitments / conventions (often no spelled-out alternatives)
7508
+ "standardize",
7509
+ "standardise",
7510
+ "standardizing",
7511
+ "moving to ",
7512
+ "migrate to",
7513
+ "migrate from",
7514
+ "defaults to",
7515
+ "default to",
7516
+ "stick with",
7517
+ "locking in",
7518
+ "lock in"
7519
+ ];
7520
+ var AFFIRMATION_PATTERNS = [
7521
+ /\b(yes|yep|yeah|yup|sure)\b/i,
7522
+ /\b(agreed|agree|approved?)\b/i,
7523
+ /\blet['’]s\s+(do|go|use|try|ship|stick|adopt|switch|move|migrate)\b/i,
7524
+ /\b(go|going)\s+with\b/i,
7525
+ /\bsounds\s+good\b/i,
7526
+ /\bdo\s+it\b/i,
7527
+ /\bship\s+it\b/i
7528
+ ];
7529
+ var IMPLICIT_ACTION_PATTERNS = [
7530
+ /import .+ from ['"][^'"]+['"]/,
7531
+ /require\(['"][^'"]+['"]\)/,
7532
+ /npm install|pnpm add|yarn add/
7533
+ ];
7534
+ var IMPLICIT_USER_COMMITMENT_PATTERNS = [
7535
+ /\blet['’]s\s+(standardize|standardise|use|go with|stick with|adopt)\b/i,
7536
+ /\b(from now on|going forward)\b/i,
7537
+ /\b(always|never)\s+(use|write|add|call|prefer|put|name|run|import|suggest|commit|push|merge|install|mock)\b/i,
7538
+ /\b(don['’]t|do not|stop|no longer)\s+(use|using|write|writing|add|adding|call|calling|suggest|suggesting|import|importing)\b/i
7539
+ ];
7540
+ exports.BROAD_MODE_MIN_USER_CHARS = 24;
7541
+ function captureGate2(turn, settings) {
7542
+ const userText = turn.user_message.toLowerCase();
7543
+ const assistantText = turn.claude_reply.toLowerCase();
7544
+ const userAffirmed = AFFIRMATION_PATTERNS.some((p) => p.test(turn.user_message));
7545
+ const keywordInUser = exports.DECISION_KEYWORDS.some((kw) => userText.includes(kw));
7546
+ const keywordInAssistant = !keywordInUser && exports.DECISION_KEYWORDS.some((kw) => assistantText.includes(kw));
7547
+ if (keywordInUser || keywordInAssistant && userAffirmed) {
7548
+ return { reachesModel: true, reason: "keyword" };
7549
+ }
7550
+ const implicitHit = IMPLICIT_ACTION_PATTERNS.some((p) => p.test(`${turn.user_message} ${turn.claude_reply}`)) || IMPLICIT_USER_COMMITMENT_PATTERNS.some((p) => p.test(turn.user_message));
7551
+ if (implicitHit)
7552
+ return { reachesModel: true, reason: "implicit" };
7553
+ if (settings?.mode === "broad" && turn.user_message.trim().length >= exports.BROAD_MODE_MIN_USER_CHARS) {
7554
+ return { reachesModel: true, reason: "broad" };
7555
+ }
7556
+ return { reachesModel: false, reason: "none" };
7292
7557
  }
7293
7558
  }
7294
7559
  });
@@ -7336,6 +7601,8 @@ var require_dist = __commonJS({
7336
7601
  __exportStar(require_veto_match(), exports);
7337
7602
  __exportStar(require_trust(), exports);
7338
7603
  __exportStar(require_extract_decision(), exports);
7604
+ __exportStar(require_capture_settings(), exports);
7605
+ __exportStar(require_capture_gate(), exports);
7339
7606
  __exportStar(require_db(), exports);
7340
7607
  }
7341
7608
  });
@@ -22798,6 +23065,9 @@ var init_config = __esm({
22798
23065
  perceptionApiUrl: (0, import_shared.normalizeLoopbackUrl)(process.env.PERCEPTION_API_URL ?? "http://127.0.0.1:3001"),
22799
23066
  perceptionApiKey: process.env.PERCEPTION_API_KEY ?? "",
22800
23067
  // ── Classifier thresholds (optional — tune after instrumenting) ─
23068
+ // DECISION_CONFIDENCE_MIN is this machine's stock value; a project can
23069
+ // override it for everyone with `robrain capture set --min-confidence`,
23070
+ // which wins when set (same for Perception's own re-extraction).
22801
23071
  decisionConfidenceMin: Number(process.env.DECISION_CONFIDENCE_MIN ?? 0.6),
22802
23072
  decisionConfidenceHigh: Number(process.env.DECISION_CONFIDENCE_HIGH ?? 0.9),
22803
23073
  topicShiftThreshold: Number(process.env.TOPIC_SHIFT_THRESHOLD ?? 0.35),
@@ -22895,23 +23165,65 @@ var init_embeddings = __esm({
22895
23165
  }
22896
23166
  });
22897
23167
 
23168
+ // src/capture-settings.ts
23169
+ async function fetchCaptureSettings(projectId) {
23170
+ if (!config2.perceptionApiUrl) return { ...import_shared4.DEFAULT_CAPTURE_SETTINGS };
23171
+ try {
23172
+ const res = await fetch(
23173
+ `${config2.perceptionApiUrl}/projects/${encodeURIComponent(projectId)}/capture`,
23174
+ {
23175
+ headers: { Authorization: `Bearer ${config2.perceptionApiKey}` },
23176
+ signal: AbortSignal.timeout(config2.fetchTimeoutMs)
23177
+ }
23178
+ );
23179
+ if (!res.ok) {
23180
+ if (res.status !== 404) {
23181
+ console.error(`[Sensing] capture settings fetch failed for ${projectId}: HTTP ${res.status}`);
23182
+ }
23183
+ return { ...import_shared4.DEFAULT_CAPTURE_SETTINGS };
23184
+ }
23185
+ return (0, import_shared4.coerceCaptureSettings)(await res.json());
23186
+ } catch (err) {
23187
+ console.error("[Sensing] capture settings fetch error:", err);
23188
+ return { ...import_shared4.DEFAULT_CAPTURE_SETTINGS };
23189
+ }
23190
+ }
23191
+ async function getCaptureSettings(projectId) {
23192
+ const hit = cache.get(projectId);
23193
+ if (hit && Date.now() - hit.fetchedAt < CACHE_TTL_MS) return hit.settings;
23194
+ const pending = inFlight.get(projectId);
23195
+ if (pending) return pending;
23196
+ const request = fetchCaptureSettings(projectId).then((settings) => {
23197
+ cache.set(projectId, { settings, fetchedAt: Date.now() });
23198
+ return settings;
23199
+ }).finally(() => inFlight.delete(projectId));
23200
+ inFlight.set(projectId, request);
23201
+ return request;
23202
+ }
23203
+ var import_shared4, CACHE_TTL_MS, cache, inFlight;
23204
+ var init_capture_settings = __esm({
23205
+ "src/capture-settings.ts"() {
23206
+ "use strict";
23207
+ import_shared4 = __toESM(require_dist(), 1);
23208
+ init_config();
23209
+ CACHE_TTL_MS = 5 * 60 * 1e3;
23210
+ cache = /* @__PURE__ */ new Map();
23211
+ inFlight = /* @__PURE__ */ new Map();
23212
+ }
23213
+ });
23214
+
22898
23215
  // src/classifiers/index.ts
22899
23216
  function getLastClassifierFailure() {
22900
23217
  return lastClassifierFailure;
22901
23218
  }
22902
23219
  async function classifyDecision(turn, projectId) {
22903
- const userText = turn.user_message.toLowerCase();
22904
- const assistantText = turn.claude_reply.toLowerCase();
22905
- const userAffirmed = AFFIRMATION_PATTERNS.some((p) => p.test(turn.user_message));
22906
- const keywordInUser = DECISION_KEYWORDS.some((kw) => userText.includes(kw));
22907
- const keywordInAssistant = !keywordInUser && DECISION_KEYWORDS.some((kw) => assistantText.includes(kw));
22908
- const keywordHit = keywordInUser || keywordInAssistant && userAffirmed;
22909
- const implicitHit = !keywordHit && (IMPLICIT_ACTION_PATTERNS.some((p) => p.test(`${turn.user_message} ${turn.claude_reply}`)) || IMPLICIT_USER_COMMITMENT_PATTERNS.some((p) => p.test(turn.user_message)));
22910
- if (!keywordHit && !implicitHit) {
23220
+ const capture = await getCaptureSettings(projectId);
23221
+ const gate = (0, import_shared5.captureGate)(turn, capture);
23222
+ if (!gate.reachesModel) {
22911
23223
  return null;
22912
23224
  }
22913
- const extracted = await extractDecision(turn);
22914
- if (!extracted.decision || extracted.confidence < config2.decisionConfidenceMin) {
23225
+ const extracted = await extractDecision(turn, capture);
23226
+ if (!extracted.decision || extracted.confidence < (0, import_shared5.effectiveConfidenceMin)(capture, config2.decisionConfidenceMin)) {
22915
23227
  return null;
22916
23228
  }
22917
23229
  const scope = inferScope(turn, extracted);
@@ -22924,22 +23236,23 @@ async function classifyDecision(turn, projectId) {
22924
23236
  extracted
22925
23237
  };
22926
23238
  }
22927
- async function extractDecision(turn) {
23239
+ async function extractDecision(turn, capture) {
22928
23240
  const empty = () => ({ decision: null, rationale: null, rejected: [], confidence: 0 });
22929
23241
  try {
22930
- const extracted = await (0, import_shared4.extractDecisionLlm)(turn.user_message, turn.claude_reply, {
23242
+ const extracted = await (0, import_shared5.extractDecisionLlm)(turn.user_message, turn.claude_reply, {
22931
23243
  provider: config2.llmProvider,
22932
23244
  anthropicApiKey: config2.anthropicApiKey,
22933
23245
  anthropicModel: config2.anthropicModel,
22934
23246
  openaiApiKey: config2.openaiApiKey,
22935
23247
  openaiModel: config2.openaiLlmModel,
22936
23248
  openaiBaseUrl: config2.openaiBaseUrl,
22937
- extraSystemRules: import_shared4.SELF_HOSTED_USER_RULE_OVERRIDE
23249
+ extraSystemRules: import_shared5.SELF_HOSTED_USER_RULE_OVERRIDE,
23250
+ capture
22938
23251
  });
22939
23252
  lastClassifierFailure = null;
22940
23253
  return extracted;
22941
23254
  } catch (err) {
22942
- if (err instanceof import_shared4.LlmKeyMissingError) {
23255
+ if (err instanceof import_shared5.LlmKeyMissingError) {
22943
23256
  console.error(`[Sensing] ${err.message} \u2014 skipping decision extraction`);
22944
23257
  return empty();
22945
23258
  }
@@ -23014,67 +23327,15 @@ function clearSessionEmbeddings(sessionId) {
23014
23327
  embeddingWindows.delete(sessionId);
23015
23328
  recentFilesPerSession.delete(sessionId);
23016
23329
  }
23017
- var import_shared4, lastClassifierFailure, DECISION_KEYWORDS, AFFIRMATION_PATTERNS, IMPLICIT_ACTION_PATTERNS, IMPLICIT_USER_COMMITMENT_PATTERNS, embeddingWindows, recentFilesPerSession;
23330
+ var import_shared5, lastClassifierFailure, embeddingWindows, recentFilesPerSession;
23018
23331
  var init_classifiers = __esm({
23019
23332
  "src/classifiers/index.ts"() {
23020
23333
  "use strict";
23021
- import_shared4 = __toESM(require_dist(), 1);
23334
+ import_shared5 = __toESM(require_dist(), 1);
23022
23335
  init_config();
23023
23336
  init_embeddings();
23337
+ init_capture_settings();
23024
23338
  lastClassifierFailure = null;
23025
- DECISION_KEYWORDS = [
23026
- "decided",
23027
- "decision",
23028
- "going with",
23029
- "we'll use",
23030
- "let's use",
23031
- "instead of",
23032
- "switched to",
23033
- "switching to",
23034
- "rejected",
23035
- "won't use",
23036
- "we tried",
23037
- "too slow",
23038
- "doesn't work",
23039
- "went with",
23040
- "chosen",
23041
- "settled on",
23042
- "ruling out",
23043
- "dropping",
23044
- "replacing",
23045
- // Casual commitments / conventions (often no spelled-out alternatives)
23046
- "standardize",
23047
- "standardise",
23048
- "standardizing",
23049
- "moving to ",
23050
- "migrate to",
23051
- "migrate from",
23052
- "defaults to",
23053
- "default to",
23054
- "stick with",
23055
- "locking in",
23056
- "lock in"
23057
- ];
23058
- AFFIRMATION_PATTERNS = [
23059
- /\b(yes|yep|yeah|yup|sure)\b/i,
23060
- /\b(agreed|agree|approved?)\b/i,
23061
- /\blet['’]s\s+(do|go|use|try|ship|stick|adopt|switch|move|migrate)\b/i,
23062
- /\b(go|going)\s+with\b/i,
23063
- /\bsounds\s+good\b/i,
23064
- /\bdo\s+it\b/i,
23065
- /\bship\s+it\b/i
23066
- ];
23067
- IMPLICIT_ACTION_PATTERNS = [
23068
- /import .+ from ['"][^'"]+['"]/,
23069
- /require\(['"][^'"]+['"]\)/,
23070
- /npm install|pnpm add|yarn add/
23071
- ];
23072
- IMPLICIT_USER_COMMITMENT_PATTERNS = [
23073
- /\blet['’]s\s+(standardize|standardise|use|go with|stick with|adopt)\b/i,
23074
- /\b(from now on|going forward)\b/i,
23075
- /\b(always|never)\s+(use|write|add|call|prefer|put|name|run|import|suggest|commit|push|merge|install|mock)\b/i,
23076
- /\b(don['’]t|do not|stop|no longer)\s+(use|using|write|writing|add|adding|call|calling|suggest|suggesting|import|importing)\b/i
23077
- ];
23078
23339
  embeddingWindows = /* @__PURE__ */ new Map();
23079
23340
  recentFilesPerSession = /* @__PURE__ */ new Map();
23080
23341
  }
@@ -23364,7 +23625,7 @@ function resolveSessionId(raw) {
23364
23625
  function buildServer() {
23365
23626
  const server = new McpServer({
23366
23627
  name: "sensing-mcp",
23367
- version: "2.4.11"
23628
+ version: "2.5.0"
23368
23629
  });
23369
23630
  server.tool(
23370
23631
  "sensing_start_session",
@@ -23384,6 +23645,7 @@ function buildServer() {
23384
23645
  const sessionId = resolveSessionId(session_id);
23385
23646
  sessionRegistry.register(sessionId, project_id);
23386
23647
  const summaryResult = await fetchAlwaysOnSummary(project_id);
23648
+ if (!isThinMode()) void getCaptureSettings(project_id);
23387
23649
  console.error(`[Sensing] Session started: ${sessionId} (${project_id})`);
23388
23650
  return {
23389
23651
  content: [{
@@ -23431,9 +23693,9 @@ function buildServer() {
23431
23693
  console.error(`[Sensing] Unknown session adopted: ${session_id} (${session.project_id})`);
23432
23694
  }
23433
23695
  }
23434
- const redactedUser = (0, import_shared5.redactSecrets)(user_message);
23435
- const redactedReply = (0, import_shared5.redactSecrets)(claude_reply);
23436
- const secretsRedacted = (0, import_shared5.totalRedactions)(redactedUser.redactions) + (0, import_shared5.totalRedactions)(redactedReply.redactions);
23696
+ const redactedUser = (0, import_shared6.redactSecrets)(user_message);
23697
+ const redactedReply = (0, import_shared6.redactSecrets)(claude_reply);
23698
+ const secretsRedacted = (0, import_shared6.totalRedactions)(redactedUser.redactions) + (0, import_shared6.totalRedactions)(redactedReply.redactions);
23437
23699
  if (secretsRedacted > 0) {
23438
23700
  console.error(`[Sensing] Redacted ${secretsRedacted} secret(s) from turn ${sequence} before buffering`);
23439
23701
  }
@@ -23650,17 +23912,18 @@ async function fetchAlwaysOnSummary(projectId) {
23650
23912
  return { text: `Project: ${projectId}` };
23651
23913
  }
23652
23914
  }
23653
- var import_shared5, sessionRegistry, lastDecisionShipFailure, pendingConflictNotices;
23915
+ var import_shared6, sessionRegistry, lastDecisionShipFailure, pendingConflictNotices;
23654
23916
  var init_server3 = __esm({
23655
23917
  "src/server.ts"() {
23656
23918
  "use strict";
23657
23919
  init_mcp();
23658
23920
  init_zod();
23659
- import_shared5 = __toESM(require_dist(), 1);
23921
+ import_shared6 = __toESM(require_dist(), 1);
23660
23922
  init_buffer();
23661
23923
  init_classifiers();
23662
23924
  init_router();
23663
23925
  init_config();
23926
+ init_capture_settings();
23664
23927
  init_pending_notices();
23665
23928
  init_session_registry();
23666
23929
  sessionRegistry = new SessionRegistry(config2.sessionRegistryPath);
@@ -23763,8 +24026,8 @@ var StdioServerTransport = class {
23763
24026
  };
23764
24027
 
23765
24028
  // src/index.ts
23766
- var import_shared6 = __toESM(require_dist(), 1);
23767
- (0, import_shared6.loadEnv)();
24029
+ var import_shared7 = __toESM(require_dist(), 1);
24030
+ (0, import_shared7.loadEnv)();
23768
24031
  process.on("unhandledRejection", (reason) => {
23769
24032
  console.error("[Sensing] Unhandled rejection (process kept alive):", reason);
23770
24033
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robrain/sensing-mcp",
3
- "version": "2.4.11",
3
+ "version": "2.5.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js"