robrain 2.4.11 → 2.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.
Files changed (45) hide show
  1. package/dist/commands/capture.d.ts +96 -0
  2. package/dist/commands/capture.d.ts.map +1 -0
  3. package/dist/commands/capture.js +356 -0
  4. package/dist/commands/capture.js.map +1 -0
  5. package/dist/commands/capture.test.d.ts +2 -0
  6. package/dist/commands/capture.test.d.ts.map +1 -0
  7. package/dist/commands/capture.test.js +105 -0
  8. package/dist/commands/capture.test.js.map +1 -0
  9. package/dist/commands/check.d.ts +5 -0
  10. package/dist/commands/check.d.ts.map +1 -1
  11. package/dist/commands/check.js +15 -0
  12. package/dist/commands/check.js.map +1 -1
  13. package/dist/commands/governance.d.ts +32 -0
  14. package/dist/commands/governance.d.ts.map +1 -0
  15. package/dist/commands/governance.js +181 -0
  16. package/dist/commands/governance.js.map +1 -0
  17. package/dist/commands/governance.test.d.ts +2 -0
  18. package/dist/commands/governance.test.d.ts.map +1 -0
  19. package/dist/commands/governance.test.js +42 -0
  20. package/dist/commands/governance.test.js.map +1 -0
  21. package/dist/commands/install.js +2 -2
  22. package/dist/commands/install.js.map +1 -1
  23. package/dist/commands/projects.d.ts +40 -0
  24. package/dist/commands/projects.d.ts.map +1 -1
  25. package/dist/commands/projects.js +168 -2
  26. package/dist/commands/projects.js.map +1 -1
  27. package/dist/commands/review.d.ts.map +1 -1
  28. package/dist/commands/review.js +6 -4
  29. package/dist/commands/review.js.map +1 -1
  30. package/dist/commands/search.d.ts +34 -0
  31. package/dist/commands/search.d.ts.map +1 -0
  32. package/dist/commands/search.js +100 -0
  33. package/dist/commands/search.js.map +1 -0
  34. package/dist/commands/search.test.d.ts +2 -0
  35. package/dist/commands/search.test.d.ts.map +1 -0
  36. package/dist/commands/search.test.js +43 -0
  37. package/dist/commands/search.test.js.map +1 -0
  38. package/dist/index.js +119 -2
  39. package/dist/index.js.map +1 -1
  40. package/package.json +2 -2
  41. package/vendor/codex-hooks/user-prompt-submit.mjs +14 -1
  42. package/vendor/sensing-mcp/dist/index.js +430 -95
  43. package/vendor/sensing-mcp/package.json +1 -1
  44. package/vendor/synthesis/dist/index.js +317 -16
  45. package/vendor/synthesis/package.json +1 -1
@@ -6589,22 +6589,26 @@ var require_embeddings = __commonJS({
6589
6589
  }
6590
6590
  };
6591
6591
  exports.EmbeddingProviderError = EmbeddingProviderError;
6592
- async function embed2(text, config3, signal) {
6593
- let vec;
6592
+ async function embed2(text, config3, signal, onUsage) {
6593
+ let out;
6594
6594
  switch (config3.provider) {
6595
6595
  case "openai":
6596
- vec = await embedOpenAI(text, config3, signal);
6596
+ out = await embedOpenAI(text, config3, signal);
6597
6597
  break;
6598
6598
  case "voyage":
6599
- vec = await embedVoyage(text, config3, signal);
6599
+ out = await embedVoyage(text, config3, signal);
6600
6600
  break;
6601
6601
  case "cohere":
6602
- vec = await embedCohere(text, config3, signal);
6602
+ out = await embedCohere(text, config3, signal);
6603
6603
  break;
6604
6604
  default:
6605
6605
  throw new EmbeddingProviderError(config3.provider, `Unknown embedding provider: ${config3.provider}`);
6606
6606
  }
6607
- return padToLength(vec, exports.EMBEDDING_TARGET_DIMS);
6607
+ onUsage?.({ inputTokens: out.inputTokens });
6608
+ return padToLength(out.vector, exports.EMBEDDING_TARGET_DIMS);
6609
+ }
6610
+ function tokenCount(x) {
6611
+ return typeof x === "number" && Number.isFinite(x) && x >= 0 ? Math.round(x) : null;
6608
6612
  }
6609
6613
  async function embedOpenAI(text, config3, signal) {
6610
6614
  const baseUrl = config3.openaiBaseUrl.replace(/\/+$/, "");
@@ -6624,7 +6628,10 @@ var require_embeddings = __commonJS({
6624
6628
  })
6625
6629
  }, "openai", config3, signal);
6626
6630
  const data = await res.json();
6627
- return ensureEmbedding("openai", data.data?.[0]?.embedding);
6631
+ return {
6632
+ vector: ensureEmbedding("openai", data.data?.[0]?.embedding),
6633
+ inputTokens: tokenCount(data.usage?.prompt_tokens) ?? tokenCount(data.usage?.total_tokens)
6634
+ };
6628
6635
  }
6629
6636
  async function embedVoyage(text, config3, signal) {
6630
6637
  if (!config3.voyageApiKey) {
@@ -6642,7 +6649,10 @@ var require_embeddings = __commonJS({
6642
6649
  })
6643
6650
  }, "voyage", config3, signal);
6644
6651
  const data = await res.json();
6645
- return ensureEmbedding("voyage", data.data?.[0]?.embedding);
6652
+ return {
6653
+ vector: ensureEmbedding("voyage", data.data?.[0]?.embedding),
6654
+ inputTokens: tokenCount(data.usage?.total_tokens)
6655
+ };
6646
6656
  }
6647
6657
  async function embedCohere(text, config3, signal) {
6648
6658
  if (!config3.cohereApiKey) {
@@ -6662,7 +6672,10 @@ var require_embeddings = __commonJS({
6662
6672
  })
6663
6673
  }, "cohere", config3, signal);
6664
6674
  const data = await res.json();
6665
- return ensureEmbedding("cohere", data.embeddings?.float?.[0]);
6675
+ return {
6676
+ vector: ensureEmbedding("cohere", data.embeddings?.float?.[0]),
6677
+ inputTokens: tokenCount(data.meta?.billed_units?.input_tokens)
6678
+ };
6666
6679
  }
6667
6680
  async function fetchEmbedding(url, init, provider, config3, callerSignal) {
6668
6681
  let lastDetail = "";
@@ -7190,16 +7203,179 @@ var require_trust = __commonJS({
7190
7203
  }
7191
7204
  });
7192
7205
 
7206
+ // ../shared/dist/capture-settings.js
7207
+ var require_capture_settings = __commonJS({
7208
+ "../shared/dist/capture-settings.js"(exports) {
7209
+ "use strict";
7210
+ Object.defineProperty(exports, "__esModule", { value: true });
7211
+ 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;
7212
+ exports.isDefaultCaptureSettings = isDefaultCaptureSettings;
7213
+ exports.normalizeKindName = normalizeKindName;
7214
+ exports.applyCaptureSettingsPatch = applyCaptureSettingsPatch;
7215
+ exports.coerceCaptureSettings = coerceCaptureSettings2;
7216
+ exports.subjectTokens = subjectTokens;
7217
+ exports.subjectsMatch = subjectsMatch;
7218
+ exports.effectiveConfidenceMin = effectiveConfidenceMin2;
7219
+ exports.allowedKindNames = allowedKindNames;
7220
+ exports.CAPTURE_INSTRUCTIONS_MAX_CHARS = 2e3;
7221
+ exports.CAPTURE_KINDS_MAX = 8;
7222
+ exports.CAPTURE_KIND_NAME_MAX_CHARS = 32;
7223
+ exports.CAPTURE_KIND_DESCRIPTION_MAX_CHARS = 300;
7224
+ exports.CAPTURE_MODES = ["strict", "broad"];
7225
+ exports.DEFAULT_CAPTURE_KIND = "decision";
7226
+ exports.DEFAULT_CAPTURE_SETTINGS = {
7227
+ instructions: null,
7228
+ mode: "strict",
7229
+ confidence_min: null,
7230
+ kinds: []
7231
+ };
7232
+ function isDefaultCaptureSettings(s) {
7233
+ return s.instructions === null && s.mode === "strict" && s.confidence_min === null && s.kinds.length === 0;
7234
+ }
7235
+ function normalizeKindName(name) {
7236
+ return name.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
7237
+ }
7238
+ function applyCaptureSettingsPatch(current, patch) {
7239
+ const errors = [];
7240
+ const next = { ...current, kinds: [...current.kinds] };
7241
+ if (patch.instructions !== void 0) {
7242
+ if (patch.instructions === null || patch.instructions.trim() === "") {
7243
+ next.instructions = null;
7244
+ } else if (typeof patch.instructions !== "string") {
7245
+ errors.push("instructions must be text");
7246
+ } else if (patch.instructions.length > exports.CAPTURE_INSTRUCTIONS_MAX_CHARS) {
7247
+ errors.push(`instructions must be at most ${exports.CAPTURE_INSTRUCTIONS_MAX_CHARS} characters (got ${patch.instructions.length})`);
7248
+ } else {
7249
+ next.instructions = patch.instructions.trim();
7250
+ }
7251
+ }
7252
+ if (patch.mode !== void 0) {
7253
+ const mode = String(patch.mode).trim().toLowerCase();
7254
+ if (exports.CAPTURE_MODES.includes(mode)) {
7255
+ next.mode = mode;
7256
+ } else {
7257
+ errors.push(`mode must be one of: ${exports.CAPTURE_MODES.join(", ")}`);
7258
+ }
7259
+ }
7260
+ if (patch.confidence_min !== void 0) {
7261
+ if (patch.confidence_min === null) {
7262
+ next.confidence_min = null;
7263
+ } else if (typeof patch.confidence_min !== "number" || !Number.isFinite(patch.confidence_min)) {
7264
+ errors.push("confidence_min must be a number between 0 and 1");
7265
+ } else if (patch.confidence_min < 0 || patch.confidence_min > 1) {
7266
+ errors.push("confidence_min must be between 0 and 1");
7267
+ } else {
7268
+ next.confidence_min = Math.round(patch.confidence_min * 100) / 100;
7269
+ }
7270
+ }
7271
+ if (patch.kinds !== void 0) {
7272
+ if (patch.kinds === null) {
7273
+ next.kinds = [];
7274
+ } else if (!Array.isArray(patch.kinds)) {
7275
+ errors.push("kinds must be a list");
7276
+ } else if (patch.kinds.length > exports.CAPTURE_KINDS_MAX) {
7277
+ errors.push(`at most ${exports.CAPTURE_KINDS_MAX} kinds`);
7278
+ } else {
7279
+ const seen = /* @__PURE__ */ new Set();
7280
+ const kinds = [];
7281
+ for (const raw of patch.kinds) {
7282
+ const name = typeof raw?.name === "string" ? normalizeKindName(raw.name) : "";
7283
+ const description = typeof raw?.description === "string" ? raw.description.trim() : "";
7284
+ if (!name) {
7285
+ errors.push("each kind needs a name (letters, digits, - or _)");
7286
+ continue;
7287
+ }
7288
+ if (name.length > exports.CAPTURE_KIND_NAME_MAX_CHARS) {
7289
+ errors.push(`kind name "${name}" is longer than ${exports.CAPTURE_KIND_NAME_MAX_CHARS} characters`);
7290
+ continue;
7291
+ }
7292
+ if (name === exports.DEFAULT_CAPTURE_KIND) {
7293
+ errors.push(`"${exports.DEFAULT_CAPTURE_KIND}" is the built-in kind and cannot be redefined`);
7294
+ continue;
7295
+ }
7296
+ if (!description) {
7297
+ errors.push(`kind "${name}" needs a description so the model knows what to store`);
7298
+ continue;
7299
+ }
7300
+ if (description.length > exports.CAPTURE_KIND_DESCRIPTION_MAX_CHARS) {
7301
+ errors.push(`description for kind "${name}" is longer than ${exports.CAPTURE_KIND_DESCRIPTION_MAX_CHARS} characters`);
7302
+ continue;
7303
+ }
7304
+ if (seen.has(name)) {
7305
+ errors.push(`kind "${name}" is listed twice`);
7306
+ continue;
7307
+ }
7308
+ seen.add(name);
7309
+ kinds.push({ name, description });
7310
+ }
7311
+ next.kinds = kinds;
7312
+ }
7313
+ }
7314
+ return errors.length ? { ok: false, errors } : { ok: true, settings: next };
7315
+ }
7316
+ function coerceCaptureSettings2(raw) {
7317
+ if (!raw || typeof raw !== "object")
7318
+ return { ...exports.DEFAULT_CAPTURE_SETTINGS };
7319
+ const r = raw;
7320
+ const result = applyCaptureSettingsPatch(exports.DEFAULT_CAPTURE_SETTINGS, {
7321
+ instructions: typeof r.instructions === "string" ? r.instructions : null,
7322
+ mode: typeof r.mode === "string" ? r.mode : "strict",
7323
+ confidence_min: typeof r.confidence_min === "number" ? r.confidence_min : null,
7324
+ kinds: Array.isArray(r.kinds) ? r.kinds : []
7325
+ });
7326
+ if (result.ok)
7327
+ return result.settings;
7328
+ const safe = { ...exports.DEFAULT_CAPTURE_SETTINGS };
7329
+ const instructions = applyCaptureSettingsPatch(safe, { instructions: typeof r.instructions === "string" ? r.instructions : null });
7330
+ if (instructions.ok)
7331
+ safe.instructions = instructions.settings.instructions;
7332
+ const mode = applyCaptureSettingsPatch(safe, { mode: typeof r.mode === "string" ? r.mode : "strict" });
7333
+ if (mode.ok)
7334
+ safe.mode = mode.settings.mode;
7335
+ const conf = applyCaptureSettingsPatch(safe, { confidence_min: typeof r.confidence_min === "number" ? r.confidence_min : null });
7336
+ if (conf.ok)
7337
+ safe.confidence_min = conf.settings.confidence_min;
7338
+ const kinds = applyCaptureSettingsPatch(safe, { kinds: Array.isArray(r.kinds) ? r.kinds : [] });
7339
+ if (kinds.ok)
7340
+ safe.kinds = kinds.settings.kinds;
7341
+ return safe;
7342
+ }
7343
+ var SUBJECT_STOPWORDS = /* @__PURE__ */ new Set(["the", "a", "an", "of", "for", "on", "in", "at", "to", "and", "this", "that", "our", "my", "its", "last", "latest", "current"]);
7344
+ function subjectTokens(subject) {
7345
+ return subject.toLowerCase().replace(/[^a-z0-9\s_-]+/g, " ").split(/[\s_-]+/).filter((t) => t.length > 1 && !SUBJECT_STOPWORDS.has(t));
7346
+ }
7347
+ function subjectsMatch(a, b) {
7348
+ const ta = new Set(subjectTokens(a));
7349
+ const tb = new Set(subjectTokens(b));
7350
+ if (ta.size === 0 || tb.size === 0)
7351
+ return false;
7352
+ const [small, large] = ta.size <= tb.size ? [ta, tb] : [tb, ta];
7353
+ for (const t of small)
7354
+ if (!large.has(t))
7355
+ return false;
7356
+ return true;
7357
+ }
7358
+ function effectiveConfidenceMin2(settings, stock) {
7359
+ return settings?.confidence_min ?? stock;
7360
+ }
7361
+ function allowedKindNames(settings) {
7362
+ return [exports.DEFAULT_CAPTURE_KIND, ...(settings?.kinds ?? []).map((k) => k.name)];
7363
+ }
7364
+ }
7365
+ });
7366
+
7193
7367
  // ../shared/dist/extract-decision.js
7194
7368
  var require_extract_decision = __commonJS({
7195
7369
  "../shared/dist/extract-decision.js"(exports) {
7196
7370
  "use strict";
7197
7371
  Object.defineProperty(exports, "__esModule", { value: true });
7198
7372
  exports.LlmKeyMissingError = exports.SELF_HOSTED_USER_RULE_OVERRIDE = exports.DECISION_EXTRACTION_SYSTEM_PROMPT = void 0;
7373
+ exports.buildCaptureRules = buildCaptureRules;
7199
7374
  exports.stripMarkdownJsonFence = stripMarkdownJsonFence;
7200
7375
  exports.parseExtractedDecision = parseExtractedDecision;
7201
7376
  exports.extractDecisionLlm = extractDecisionLlm2;
7202
7377
  var llm_js_1 = require_llm();
7378
+ var capture_settings_js_1 = require_capture_settings();
7203
7379
  exports.DECISION_EXTRACTION_SYSTEM_PROMPT = `You extract technical decisions from software development conversations.
7204
7380
 
7205
7381
  A decision includes ANY of the following (not only formal deliberation):
@@ -7225,6 +7401,23 @@ Fields:
7225
7401
  Output ONLY valid JSON. If no decision: {"decision": null, "rationale": null, "rejected": [], "confidence": 0}.
7226
7402
  Never add explanation outside the JSON.
7227
7403
  Schema: {"decision": string|null, "rationale": string|null, "rejected": [{"option": string, "reason": string}], "confidence": number}`;
7404
+ function buildCaptureRules(capture) {
7405
+ if (!capture)
7406
+ return "";
7407
+ const sections = [];
7408
+ if (capture.kinds.length > 0) {
7409
+ const kindLines = capture.kinds.map((k) => `- "${k.name}": ${k.description}`).join("\n");
7410
+ sections.push(`Besides decisions, this project also stores these kinds of memory:
7411
+ ${kindLines}
7412
+ 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.
7413
+ Schema: {"decision": string|null, "rationale": string|null, "rejected": [{"option": string, "reason": string}], "confidence": number, "kind": string, "entry": string|null, "subject": string|null}`);
7414
+ }
7415
+ if (capture.instructions) {
7416
+ sections.push(`Project instructions for what to store (they refine the rules above; the output must stay valid JSON in the schema given):
7417
+ ${capture.instructions}`);
7418
+ }
7419
+ return sections.join("\n\n");
7420
+ }
7228
7421
  exports.SELF_HOSTED_USER_RULE_OVERRIDE = `User-stated rule wins:
7229
7422
  - 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
7423
  - 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 +7432,28 @@ Schema: {"decision": string|null, "rationale": string|null, "rejected": [{"optio
7239
7432
  function stripMarkdownJsonFence(raw) {
7240
7433
  return raw.trim().replace(/^```[a-z]*\s*/i, "").replace(/\s*```\s*$/, "").trim();
7241
7434
  }
7242
- function parseExtractedDecision(rawText) {
7435
+ function parseExtractedDecision(rawText, allowedKinds = [capture_settings_js_1.DEFAULT_CAPTURE_KIND]) {
7243
7436
  const raw = JSON.parse(stripMarkdownJsonFence(rawText));
7244
7437
  const rejected = Array.isArray(raw.rejected) ? raw.rejected.filter((x) => x !== null && typeof x === "object" && typeof x.option === "string" && typeof x.reason === "string") : [];
7245
7438
  const confidence = typeof raw.confidence === "number" && Number.isFinite(raw.confidence) ? raw.confidence : 0;
7439
+ const requestedKind = typeof raw.kind === "string" ? (0, capture_settings_js_1.normalizeKindName)(raw.kind) : capture_settings_js_1.DEFAULT_CAPTURE_KIND;
7440
+ const kind = allowedKinds.includes(requestedKind) ? requestedKind : capture_settings_js_1.DEFAULT_CAPTURE_KIND;
7441
+ const subject = kind !== capture_settings_js_1.DEFAULT_CAPTURE_KIND && typeof raw.subject === "string" && raw.subject.trim() ? raw.subject.trim().slice(0, 120) : null;
7442
+ const entry = kind !== capture_settings_js_1.DEFAULT_CAPTURE_KIND && typeof raw.entry === "string" && raw.entry.trim() ? raw.entry.trim() : null;
7246
7443
  return {
7247
- decision: typeof raw.decision === "string" ? raw.decision : null,
7444
+ decision: entry ?? (typeof raw.decision === "string" ? raw.decision : null),
7248
7445
  rationale: typeof raw.rationale === "string" ? raw.rationale : null,
7249
7446
  rejected,
7250
- confidence
7447
+ confidence,
7448
+ kind,
7449
+ subject
7251
7450
  };
7252
7451
  }
7253
7452
  async function extractDecisionLlm2(userMessage, claudeReply, cfg) {
7254
7453
  const userPrompt = `Session turn:
7255
7454
  User: ${userMessage}
7256
7455
  Claude: ${claudeReply}`;
7257
- const systemPrompt = cfg.extraSystemRules ? `${exports.DECISION_EXTRACTION_SYSTEM_PROMPT}
7258
-
7259
- ${cfg.extraSystemRules}` : exports.DECISION_EXTRACTION_SYSTEM_PROMPT;
7456
+ const systemPrompt = [exports.DECISION_EXTRACTION_SYSTEM_PROMPT, cfg.extraSystemRules, buildCaptureRules(cfg.capture)].filter((section) => Boolean(section)).join("\n\n");
7260
7457
  let rawText;
7261
7458
  if (cfg.provider === "openai") {
7262
7459
  const baseUrl = cfg.openaiBaseUrl ?? (0, llm_js_1.resolveOpenAiBaseUrl)();
@@ -7288,7 +7485,108 @@ ${cfg.extraSystemRules}` : exports.DECISION_EXTRACTION_SYSTEM_PROMPT;
7288
7485
  temperature: 0
7289
7486
  });
7290
7487
  }
7291
- return parseExtractedDecision(rawText);
7488
+ return parseExtractedDecision(rawText, (0, capture_settings_js_1.allowedKindNames)(cfg.capture));
7489
+ }
7490
+ }
7491
+ });
7492
+
7493
+ // ../shared/dist/capture-gate.js
7494
+ var require_capture_gate = __commonJS({
7495
+ "../shared/dist/capture-gate.js"(exports) {
7496
+ "use strict";
7497
+ Object.defineProperty(exports, "__esModule", { value: true });
7498
+ exports.BROAD_MODE_MIN_USER_CHARS = exports.DECISION_KEYWORDS = void 0;
7499
+ exports.captureGate = captureGate2;
7500
+ exports.DECISION_KEYWORDS = [
7501
+ "decided",
7502
+ "decision",
7503
+ "going with",
7504
+ "we'll use",
7505
+ "let's use",
7506
+ "instead of",
7507
+ "switched to",
7508
+ "switching to",
7509
+ "rejected",
7510
+ "won't use",
7511
+ "we tried",
7512
+ "too slow",
7513
+ "doesn't work",
7514
+ "went with",
7515
+ "chosen",
7516
+ "settled on",
7517
+ "ruling out",
7518
+ "dropping",
7519
+ "replacing",
7520
+ // Casual commitments / conventions (often no spelled-out alternatives)
7521
+ "standardize",
7522
+ "standardise",
7523
+ "standardizing",
7524
+ "moving to ",
7525
+ "migrate to",
7526
+ "migrate from",
7527
+ "defaults to",
7528
+ "default to",
7529
+ "stick with",
7530
+ "locking in",
7531
+ "lock in"
7532
+ ];
7533
+ var AFFIRMATION_PATTERNS = [
7534
+ /\b(yes|yep|yeah|yup|sure)\b/i,
7535
+ /\b(agreed|agree|approved?)\b/i,
7536
+ /\blet['’]s\s+(do|go|use|try|ship|stick|adopt|switch|move|migrate)\b/i,
7537
+ /\b(go|going)\s+with\b/i,
7538
+ /\bsounds\s+good\b/i,
7539
+ /\bdo\s+it\b/i,
7540
+ /\bship\s+it\b/i
7541
+ ];
7542
+ var IMPLICIT_ACTION_PATTERNS = [
7543
+ /import .+ from ['"][^'"]+['"]/,
7544
+ /require\(['"][^'"]+['"]\)/,
7545
+ /npm install|pnpm add|yarn add/
7546
+ ];
7547
+ var IMPLICIT_USER_COMMITMENT_PATTERNS = [
7548
+ /\blet['’]s\s+(standardize|standardise|use|go with|stick with|adopt)\b/i,
7549
+ /\b(from now on|going forward)\b/i,
7550
+ /\b(always|never)\s+(use|write|add|call|prefer|put|name|run|import|suggest|commit|push|merge|install|mock)\b/i,
7551
+ /\b(don['’]t|do not|stop|no longer)\s+(use|using|write|writing|add|adding|call|calling|suggest|suggesting|import|importing)\b/i
7552
+ ];
7553
+ exports.BROAD_MODE_MIN_USER_CHARS = 24;
7554
+ function captureGate2(turn, settings) {
7555
+ const userText = turn.user_message.toLowerCase();
7556
+ const assistantText = turn.claude_reply.toLowerCase();
7557
+ const userAffirmed = AFFIRMATION_PATTERNS.some((p) => p.test(turn.user_message));
7558
+ const keywordInUser = exports.DECISION_KEYWORDS.some((kw) => userText.includes(kw));
7559
+ const keywordInAssistant = !keywordInUser && exports.DECISION_KEYWORDS.some((kw) => assistantText.includes(kw));
7560
+ if (keywordInUser || keywordInAssistant && userAffirmed) {
7561
+ return { reachesModel: true, reason: "keyword" };
7562
+ }
7563
+ 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));
7564
+ if (implicitHit)
7565
+ return { reachesModel: true, reason: "implicit" };
7566
+ if (settings?.mode === "broad" && turn.user_message.trim().length >= exports.BROAD_MODE_MIN_USER_CHARS) {
7567
+ return { reachesModel: true, reason: "broad" };
7568
+ }
7569
+ return { reachesModel: false, reason: "none" };
7570
+ }
7571
+ }
7572
+ });
7573
+
7574
+ // ../shared/dist/author-email.js
7575
+ var require_author_email = __commonJS({
7576
+ "../shared/dist/author-email.js"(exports) {
7577
+ "use strict";
7578
+ Object.defineProperty(exports, "__esModule", { value: true });
7579
+ exports.AUTHOR_EMAIL_MAX_CHARS = void 0;
7580
+ exports.normalizeAuthorEmail = normalizeAuthorEmail2;
7581
+ exports.AUTHOR_EMAIL_MAX_CHARS = 254;
7582
+ var AUTHOR_EMAIL_RE = /^[a-z0-9._%+-]{1,64}@[a-z0-9-]+(\.[a-z0-9-]+)+$/;
7583
+ function normalizeAuthorEmail2(raw) {
7584
+ if (typeof raw !== "string")
7585
+ return null;
7586
+ const email2 = raw.trim().toLowerCase();
7587
+ if (!email2 || email2.length > exports.AUTHOR_EMAIL_MAX_CHARS)
7588
+ return null;
7589
+ return AUTHOR_EMAIL_RE.test(email2) ? email2 : null;
7292
7590
  }
7293
7591
  }
7294
7592
  });
@@ -7336,6 +7634,9 @@ var require_dist = __commonJS({
7336
7634
  __exportStar(require_veto_match(), exports);
7337
7635
  __exportStar(require_trust(), exports);
7338
7636
  __exportStar(require_extract_decision(), exports);
7637
+ __exportStar(require_capture_settings(), exports);
7638
+ __exportStar(require_capture_gate(), exports);
7639
+ __exportStar(require_author_email(), exports);
7339
7640
  __exportStar(require_db(), exports);
7340
7641
  }
7341
7642
  });
@@ -22798,6 +23099,9 @@ var init_config = __esm({
22798
23099
  perceptionApiUrl: (0, import_shared.normalizeLoopbackUrl)(process.env.PERCEPTION_API_URL ?? "http://127.0.0.1:3001"),
22799
23100
  perceptionApiKey: process.env.PERCEPTION_API_KEY ?? "",
22800
23101
  // ── Classifier thresholds (optional — tune after instrumenting) ─
23102
+ // DECISION_CONFIDENCE_MIN is this machine's stock value; a project can
23103
+ // override it for everyone with `robrain capture set --min-confidence`,
23104
+ // which wins when set (same for Perception's own re-extraction).
22801
23105
  decisionConfidenceMin: Number(process.env.DECISION_CONFIDENCE_MIN ?? 0.6),
22802
23106
  decisionConfidenceHigh: Number(process.env.DECISION_CONFIDENCE_HIGH ?? 0.9),
22803
23107
  topicShiftThreshold: Number(process.env.TOPIC_SHIFT_THRESHOLD ?? 0.35),
@@ -22895,23 +23199,65 @@ var init_embeddings = __esm({
22895
23199
  }
22896
23200
  });
22897
23201
 
23202
+ // src/capture-settings.ts
23203
+ async function fetchCaptureSettings(projectId) {
23204
+ if (!config2.perceptionApiUrl) return { ...import_shared4.DEFAULT_CAPTURE_SETTINGS };
23205
+ try {
23206
+ const res = await fetch(
23207
+ `${config2.perceptionApiUrl}/projects/${encodeURIComponent(projectId)}/capture`,
23208
+ {
23209
+ headers: { Authorization: `Bearer ${config2.perceptionApiKey}` },
23210
+ signal: AbortSignal.timeout(config2.fetchTimeoutMs)
23211
+ }
23212
+ );
23213
+ if (!res.ok) {
23214
+ if (res.status !== 404) {
23215
+ console.error(`[Sensing] capture settings fetch failed for ${projectId}: HTTP ${res.status}`);
23216
+ }
23217
+ return { ...import_shared4.DEFAULT_CAPTURE_SETTINGS };
23218
+ }
23219
+ return (0, import_shared4.coerceCaptureSettings)(await res.json());
23220
+ } catch (err) {
23221
+ console.error("[Sensing] capture settings fetch error:", err);
23222
+ return { ...import_shared4.DEFAULT_CAPTURE_SETTINGS };
23223
+ }
23224
+ }
23225
+ async function getCaptureSettings(projectId) {
23226
+ const hit = cache.get(projectId);
23227
+ if (hit && Date.now() - hit.fetchedAt < CACHE_TTL_MS) return hit.settings;
23228
+ const pending = inFlight.get(projectId);
23229
+ if (pending) return pending;
23230
+ const request = fetchCaptureSettings(projectId).then((settings) => {
23231
+ cache.set(projectId, { settings, fetchedAt: Date.now() });
23232
+ return settings;
23233
+ }).finally(() => inFlight.delete(projectId));
23234
+ inFlight.set(projectId, request);
23235
+ return request;
23236
+ }
23237
+ var import_shared4, CACHE_TTL_MS, cache, inFlight;
23238
+ var init_capture_settings = __esm({
23239
+ "src/capture-settings.ts"() {
23240
+ "use strict";
23241
+ import_shared4 = __toESM(require_dist(), 1);
23242
+ init_config();
23243
+ CACHE_TTL_MS = 5 * 60 * 1e3;
23244
+ cache = /* @__PURE__ */ new Map();
23245
+ inFlight = /* @__PURE__ */ new Map();
23246
+ }
23247
+ });
23248
+
22898
23249
  // src/classifiers/index.ts
22899
23250
  function getLastClassifierFailure() {
22900
23251
  return lastClassifierFailure;
22901
23252
  }
22902
23253
  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) {
23254
+ const capture = await getCaptureSettings(projectId);
23255
+ const gate = (0, import_shared5.captureGate)(turn, capture);
23256
+ if (!gate.reachesModel) {
22911
23257
  return null;
22912
23258
  }
22913
- const extracted = await extractDecision(turn);
22914
- if (!extracted.decision || extracted.confidence < config2.decisionConfidenceMin) {
23259
+ const extracted = await extractDecision(turn, capture);
23260
+ if (!extracted.decision || extracted.confidence < (0, import_shared5.effectiveConfidenceMin)(capture, config2.decisionConfidenceMin)) {
22915
23261
  return null;
22916
23262
  }
22917
23263
  const scope = inferScope(turn, extracted);
@@ -22924,22 +23270,23 @@ async function classifyDecision(turn, projectId) {
22924
23270
  extracted
22925
23271
  };
22926
23272
  }
22927
- async function extractDecision(turn) {
23273
+ async function extractDecision(turn, capture) {
22928
23274
  const empty = () => ({ decision: null, rationale: null, rejected: [], confidence: 0 });
22929
23275
  try {
22930
- const extracted = await (0, import_shared4.extractDecisionLlm)(turn.user_message, turn.claude_reply, {
23276
+ const extracted = await (0, import_shared5.extractDecisionLlm)(turn.user_message, turn.claude_reply, {
22931
23277
  provider: config2.llmProvider,
22932
23278
  anthropicApiKey: config2.anthropicApiKey,
22933
23279
  anthropicModel: config2.anthropicModel,
22934
23280
  openaiApiKey: config2.openaiApiKey,
22935
23281
  openaiModel: config2.openaiLlmModel,
22936
23282
  openaiBaseUrl: config2.openaiBaseUrl,
22937
- extraSystemRules: import_shared4.SELF_HOSTED_USER_RULE_OVERRIDE
23283
+ extraSystemRules: import_shared5.SELF_HOSTED_USER_RULE_OVERRIDE,
23284
+ capture
22938
23285
  });
22939
23286
  lastClassifierFailure = null;
22940
23287
  return extracted;
22941
23288
  } catch (err) {
22942
- if (err instanceof import_shared4.LlmKeyMissingError) {
23289
+ if (err instanceof import_shared5.LlmKeyMissingError) {
22943
23290
  console.error(`[Sensing] ${err.message} \u2014 skipping decision extraction`);
22944
23291
  return empty();
22945
23292
  }
@@ -23014,73 +23361,56 @@ function clearSessionEmbeddings(sessionId) {
23014
23361
  embeddingWindows.delete(sessionId);
23015
23362
  recentFilesPerSession.delete(sessionId);
23016
23363
  }
23017
- var import_shared4, lastClassifierFailure, DECISION_KEYWORDS, AFFIRMATION_PATTERNS, IMPLICIT_ACTION_PATTERNS, IMPLICIT_USER_COMMITMENT_PATTERNS, embeddingWindows, recentFilesPerSession;
23364
+ var import_shared5, lastClassifierFailure, embeddingWindows, recentFilesPerSession;
23018
23365
  var init_classifiers = __esm({
23019
23366
  "src/classifiers/index.ts"() {
23020
23367
  "use strict";
23021
- import_shared4 = __toESM(require_dist(), 1);
23368
+ import_shared5 = __toESM(require_dist(), 1);
23022
23369
  init_config();
23023
23370
  init_embeddings();
23371
+ init_capture_settings();
23024
23372
  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
23373
  embeddingWindows = /* @__PURE__ */ new Map();
23079
23374
  recentFilesPerSession = /* @__PURE__ */ new Map();
23080
23375
  }
23081
23376
  });
23082
23377
 
23378
+ // src/author.ts
23379
+ import { execFile } from "node:child_process";
23380
+ function normalizeEmail(raw) {
23381
+ return (0, import_shared6.normalizeAuthorEmail)(raw);
23382
+ }
23383
+ function gitUserEmail(cwd) {
23384
+ return new Promise((resolve) => {
23385
+ try {
23386
+ execFile("git", ["config", "--get", "user.email"], { cwd, timeout: GIT_TIMEOUT_MS }, (err, stdout) => {
23387
+ resolve(err ? null : normalizeEmail(String(stdout)));
23388
+ });
23389
+ } catch {
23390
+ resolve(null);
23391
+ }
23392
+ });
23393
+ }
23394
+ function resolveAuthorEmail(env = process.env, cwd = process.cwd()) {
23395
+ if (!cached2) {
23396
+ cached2 = (async () => normalizeEmail(env.ROBRAIN_AUTHOR_EMAIL) ?? await gitUserEmail(cwd))();
23397
+ }
23398
+ return cached2;
23399
+ }
23400
+ var import_shared6, GIT_TIMEOUT_MS, cached2;
23401
+ var init_author = __esm({
23402
+ "src/author.ts"() {
23403
+ "use strict";
23404
+ import_shared6 = __toESM(require_dist(), 1);
23405
+ GIT_TIMEOUT_MS = 2e3;
23406
+ }
23407
+ });
23408
+
23083
23409
  // src/router.ts
23410
+ async function withAuthor(signal) {
23411
+ const authorEmail = await resolveAuthorEmail();
23412
+ return authorEmail ? { ...signal, author_email: authorEmail } : signal;
23413
+ }
23084
23414
  function parsePerceptionUserMessage(status, rawText) {
23085
23415
  try {
23086
23416
  const j = JSON.parse(rawText);
@@ -23111,7 +23441,7 @@ async function routeDecisionSignal(signal, projectId) {
23111
23441
  "Authorization": `Bearer ${config2.perceptionApiKey}`,
23112
23442
  "X-Project-Id": projectId
23113
23443
  },
23114
- body: JSON.stringify({ signal })
23444
+ body: JSON.stringify({ signal: await withAuthor(signal) })
23115
23445
  });
23116
23446
  const raw = await res.text();
23117
23447
  if (!res.ok) {
@@ -23186,6 +23516,7 @@ async function routeFlushTurns(turns, projectId) {
23186
23516
  errors.push("PERCEPTION_API_URL is not set \u2014 flush could not reach Perception.");
23187
23517
  return errors;
23188
23518
  }
23519
+ const authorEmail = await resolveAuthorEmail();
23189
23520
  const signals = turns.map((turn) => ({
23190
23521
  signal: {
23191
23522
  turn,
@@ -23193,7 +23524,8 @@ async function routeFlushTurns(turns, projectId) {
23193
23524
  confidence: 0.5,
23194
23525
  files_affected: turn.files_touched,
23195
23526
  scope: "team",
23196
- needs_classification: true
23527
+ needs_classification: true,
23528
+ ...authorEmail ? { author_email: authorEmail } : {}
23197
23529
  }
23198
23530
  }));
23199
23531
  await Promise.allSettled(
@@ -23223,6 +23555,7 @@ var init_router = __esm({
23223
23555
  "src/router.ts"() {
23224
23556
  "use strict";
23225
23557
  init_config();
23558
+ init_author();
23226
23559
  }
23227
23560
  });
23228
23561
 
@@ -23364,7 +23697,7 @@ function resolveSessionId(raw) {
23364
23697
  function buildServer() {
23365
23698
  const server = new McpServer({
23366
23699
  name: "sensing-mcp",
23367
- version: "2.4.11"
23700
+ version: "2.6.0"
23368
23701
  });
23369
23702
  server.tool(
23370
23703
  "sensing_start_session",
@@ -23384,6 +23717,7 @@ function buildServer() {
23384
23717
  const sessionId = resolveSessionId(session_id);
23385
23718
  sessionRegistry.register(sessionId, project_id);
23386
23719
  const summaryResult = await fetchAlwaysOnSummary(project_id);
23720
+ if (!isThinMode()) void getCaptureSettings(project_id);
23387
23721
  console.error(`[Sensing] Session started: ${sessionId} (${project_id})`);
23388
23722
  return {
23389
23723
  content: [{
@@ -23431,9 +23765,9 @@ function buildServer() {
23431
23765
  console.error(`[Sensing] Unknown session adopted: ${session_id} (${session.project_id})`);
23432
23766
  }
23433
23767
  }
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);
23768
+ const redactedUser = (0, import_shared7.redactSecrets)(user_message);
23769
+ const redactedReply = (0, import_shared7.redactSecrets)(claude_reply);
23770
+ const secretsRedacted = (0, import_shared7.totalRedactions)(redactedUser.redactions) + (0, import_shared7.totalRedactions)(redactedReply.redactions);
23437
23771
  if (secretsRedacted > 0) {
23438
23772
  console.error(`[Sensing] Redacted ${secretsRedacted} secret(s) from turn ${sequence} before buffering`);
23439
23773
  }
@@ -23650,17 +23984,18 @@ async function fetchAlwaysOnSummary(projectId) {
23650
23984
  return { text: `Project: ${projectId}` };
23651
23985
  }
23652
23986
  }
23653
- var import_shared5, sessionRegistry, lastDecisionShipFailure, pendingConflictNotices;
23987
+ var import_shared7, sessionRegistry, lastDecisionShipFailure, pendingConflictNotices;
23654
23988
  var init_server3 = __esm({
23655
23989
  "src/server.ts"() {
23656
23990
  "use strict";
23657
23991
  init_mcp();
23658
23992
  init_zod();
23659
- import_shared5 = __toESM(require_dist(), 1);
23993
+ import_shared7 = __toESM(require_dist(), 1);
23660
23994
  init_buffer();
23661
23995
  init_classifiers();
23662
23996
  init_router();
23663
23997
  init_config();
23998
+ init_capture_settings();
23664
23999
  init_pending_notices();
23665
24000
  init_session_registry();
23666
24001
  sessionRegistry = new SessionRegistry(config2.sessionRegistryPath);
@@ -23763,8 +24098,8 @@ var StdioServerTransport = class {
23763
24098
  };
23764
24099
 
23765
24100
  // src/index.ts
23766
- var import_shared6 = __toESM(require_dist(), 1);
23767
- (0, import_shared6.loadEnv)();
24101
+ var import_shared8 = __toESM(require_dist(), 1);
24102
+ (0, import_shared8.loadEnv)();
23768
24103
  process.on("unhandledRejection", (reason) => {
23769
24104
  console.error("[Sensing] Unhandled rejection (process kept alive):", reason);
23770
24105
  });