@agent-native/core 0.111.3 → 0.112.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 (109) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +18 -0
  3. package/corpus/core/package.json +3 -1
  4. package/corpus/core/src/cli/code-agent-executor.ts +17 -5
  5. package/corpus/core/src/cli/code-agent-runs.ts +43 -2
  6. package/corpus/core/src/cli/migration-codemod.ts +59 -9
  7. package/corpus/core/src/cli/multi-frontier-runs.ts +841 -0
  8. package/corpus/core/src/coding-tools/index.ts +27 -8
  9. package/corpus/core/src/embeddings/index.ts +233 -0
  10. package/corpus/core/src/index.ts +2 -0
  11. package/corpus/core/src/search/index.ts +413 -0
  12. package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +48 -5
  13. package/corpus/templates/brain/.agents/skills/brain/SKILL.md +46 -14
  14. package/corpus/templates/brain/README.md +34 -3
  15. package/corpus/templates/brain/actions/approve-proposal.ts +2 -0
  16. package/corpus/templates/brain/actions/claim-distillation.ts +24 -27
  17. package/corpus/templates/brain/actions/create-source.ts +1 -1
  18. package/corpus/templates/brain/actions/get-knowledge.ts +6 -2
  19. package/corpus/templates/brain/actions/get-pilot-report.ts +44 -8
  20. package/corpus/templates/brain/actions/import-capture.ts +21 -9
  21. package/corpus/templates/brain/actions/import-transcript.ts +42 -14
  22. package/corpus/templates/brain/actions/list-captures.ts +10 -1
  23. package/corpus/templates/brain/actions/list-distillation-queue.ts +39 -14
  24. package/corpus/templates/brain/actions/list-projects.ts +51 -0
  25. package/corpus/templates/brain/actions/list-proposals.ts +80 -4
  26. package/corpus/templates/brain/actions/list-sources.ts +17 -3
  27. package/corpus/templates/brain/actions/manage-project.ts +118 -0
  28. package/corpus/templates/brain/actions/mark-capture-distilled.ts +68 -20
  29. package/corpus/templates/brain/actions/navigate.ts +1 -0
  30. package/corpus/templates/brain/actions/reject-proposal.ts +6 -1
  31. package/corpus/templates/brain/actions/resanitize-captures.ts +159 -33
  32. package/corpus/templates/brain/actions/review-proposal.ts +2 -0
  33. package/corpus/templates/brain/actions/search-everything.ts +10 -1
  34. package/corpus/templates/brain/actions/search-knowledge.ts +1 -1
  35. package/corpus/templates/brain/actions/set-settings.ts +36 -1
  36. package/corpus/templates/brain/actions/update-proposal.ts +2 -0
  37. package/corpus/templates/brain/app/hooks/use-distillation-bridge.ts +18 -9
  38. package/corpus/templates/brain/app/hooks/use-navigation-state.ts +3 -0
  39. package/corpus/templates/brain/app/i18n-data.ts +731 -2
  40. package/corpus/templates/brain/app/lib/brain.ts +33 -1
  41. package/corpus/templates/brain/app/routes/ops.tsx +47 -0
  42. package/corpus/templates/brain/app/routes/review.tsx +41 -2
  43. package/corpus/templates/brain/app/routes/search.tsx +33 -2
  44. package/corpus/templates/brain/app/routes/settings.tsx +158 -0
  45. package/corpus/templates/brain/app/routes/sources.tsx +31 -0
  46. package/corpus/templates/brain/changelog/2026-07-19-added-private-source-safe-semantic-search.md +6 -0
  47. package/corpus/templates/brain/jobs/process-ingest-queue.ts +270 -36
  48. package/corpus/templates/brain/package.json +1 -0
  49. package/corpus/templates/brain/server/db/index.ts +18 -0
  50. package/corpus/templates/brain/server/db/schema.ts +197 -1
  51. package/corpus/templates/brain/server/jobs/distillation-queue.ts +12 -5
  52. package/corpus/templates/brain/server/lib/audiences.ts +708 -0
  53. package/corpus/templates/brain/server/lib/brain-health.ts +251 -38
  54. package/corpus/templates/brain/server/lib/brain.ts +877 -50
  55. package/corpus/templates/brain/server/lib/capture-sanitization.ts +217 -53
  56. package/corpus/templates/brain/server/lib/connectors.ts +556 -70
  57. package/corpus/templates/brain/server/lib/demo.ts +17 -4
  58. package/corpus/templates/brain/server/lib/hybrid-search.ts +363 -0
  59. package/corpus/templates/brain/server/lib/ingest-queue.ts +104 -0
  60. package/corpus/templates/brain/server/lib/privacy-readiness.ts +24 -0
  61. package/corpus/templates/brain/server/lib/search-index-contracts.ts +72 -0
  62. package/corpus/templates/brain/server/lib/search-index.ts +714 -0
  63. package/corpus/templates/brain/server/lib/search.ts +120 -4
  64. package/corpus/templates/brain/server/lib/sensitivity-policy.ts +174 -0
  65. package/corpus/templates/brain/server/lib/slack-events.ts +281 -0
  66. package/corpus/templates/brain/server/onboarding.ts +29 -0
  67. package/corpus/templates/brain/server/plugins/db.ts +204 -0
  68. package/corpus/templates/brain/server/register-secrets.ts +46 -0
  69. package/corpus/templates/brain/server/routes/api/_agent-native/brain/ingest.post.ts +36 -16
  70. package/corpus/templates/brain/server/routes/api/_agent-native/brain/slack-events.post.ts +28 -0
  71. package/corpus/templates/brain/shared/types.ts +10 -0
  72. package/dist/cli/code-agent-executor.d.ts +8 -9
  73. package/dist/cli/code-agent-executor.d.ts.map +1 -1
  74. package/dist/cli/code-agent-executor.js +9 -3
  75. package/dist/cli/code-agent-executor.js.map +1 -1
  76. package/dist/cli/code-agent-runs.d.ts +5 -0
  77. package/dist/cli/code-agent-runs.d.ts.map +1 -1
  78. package/dist/cli/code-agent-runs.js +33 -2
  79. package/dist/cli/code-agent-runs.js.map +1 -1
  80. package/dist/cli/migration-codemod.d.ts.map +1 -1
  81. package/dist/cli/migration-codemod.js +48 -10
  82. package/dist/cli/migration-codemod.js.map +1 -1
  83. package/dist/cli/multi-frontier-runs.d.ts +131 -0
  84. package/dist/cli/multi-frontier-runs.d.ts.map +1 -0
  85. package/dist/cli/multi-frontier-runs.js +529 -0
  86. package/dist/cli/multi-frontier-runs.js.map +1 -0
  87. package/dist/coding-tools/index.d.ts +1 -0
  88. package/dist/coding-tools/index.d.ts.map +1 -1
  89. package/dist/coding-tools/index.js +25 -7
  90. package/dist/coding-tools/index.js.map +1 -1
  91. package/dist/collab/awareness.d.ts +2 -2
  92. package/dist/collab/awareness.d.ts.map +1 -1
  93. package/dist/embeddings/index.d.ts +24 -0
  94. package/dist/embeddings/index.d.ts.map +1 -0
  95. package/dist/embeddings/index.js +172 -0
  96. package/dist/embeddings/index.js.map +1 -0
  97. package/dist/index.d.ts +2 -0
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +2 -0
  100. package/dist/index.js.map +1 -1
  101. package/dist/notifications/routes.d.ts +3 -3
  102. package/dist/progress/routes.d.ts +1 -1
  103. package/dist/resources/handlers.d.ts +1 -1
  104. package/dist/search/index.d.ts +88 -0
  105. package/dist/search/index.d.ts.map +1 -0
  106. package/dist/search/index.js +267 -0
  107. package/dist/search/index.js.map +1 -0
  108. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  109. package/package.json +3 -1
@@ -97,7 +97,13 @@ export interface ReviewItem {
97
97
  url?: string | null;
98
98
  timestampMs?: number | null;
99
99
  }>;
100
- status?: "pending" | "queued" | "approved" | "rejected" | "needs_changes";
100
+ status?:
101
+ | "pending"
102
+ | "queued"
103
+ | "approved"
104
+ | "rejected"
105
+ | "needs_changes"
106
+ | "quarantine";
101
107
  visibility?: string;
102
108
  reviewerNotes?: string | null;
103
109
  createdBy?: string | null;
@@ -182,6 +188,8 @@ export interface SearchEverythingResult {
182
188
  confidence?: number | null;
183
189
  updatedAt?: string | null;
184
190
  score?: number | null;
191
+ matchLane?: "semantic" | "keyword" | "hybrid" | string | null;
192
+ retrievalReason?: string | null;
185
193
  }
186
194
 
187
195
  export interface SearchEverythingResponse {
@@ -197,6 +205,20 @@ export interface SearchEverythingResponse {
197
205
  };
198
206
  }
199
207
 
208
+ export interface BrainProject {
209
+ id: string;
210
+ title: string;
211
+ description?: string | null;
212
+ visibility?: string | null;
213
+ sourceIds?: string[];
214
+ isDefault?: boolean;
215
+ updatedAt?: string | null;
216
+ }
217
+
218
+ export interface BrainProjectsResponse {
219
+ projects?: BrainProject[];
220
+ }
221
+
200
222
  export interface ReviewQueueResponse {
201
223
  count?: number;
202
224
  items?: ReviewItem[];
@@ -867,6 +889,11 @@ export interface BrainSettings {
867
889
  requireCitations?: boolean;
868
890
  autoArchiveResolved?: boolean;
869
891
  notifyOnSourceErrors?: boolean;
892
+ privacyClassifierModel?: string;
893
+ privacyClassifierEngine?: string;
894
+ sensitivityCustomInstructions?: string;
895
+ publicChannelExclusionPatterns?: string[];
896
+ quarantineRetentionHours?: number;
870
897
  }
871
898
 
872
899
  export interface SettingsResponse {
@@ -951,6 +978,11 @@ export const defaultSettings: BrainSettings = {
951
978
  requireCitations: true,
952
979
  autoArchiveResolved: true,
953
980
  notifyOnSourceErrors: true,
981
+ privacyClassifierModel: "",
982
+ privacyClassifierEngine: "",
983
+ sensitivityCustomInstructions: "",
984
+ publicChannelExclusionPatterns: [],
985
+ quarantineRetentionHours: 72,
954
986
  };
955
987
 
956
988
  export function viewFromPath(pathname: string): BrainView {
@@ -44,6 +44,7 @@ import {
44
44
  type BrainDistillationQueueStatus,
45
45
  type BrainOpsQueueItem,
46
46
  type BrainOpsQueueResponse,
47
+ type BrainHealthResponse,
47
48
  statusLabel,
48
49
  } from "@/lib/brain";
49
50
  import { cn } from "@/lib/utils";
@@ -135,6 +136,21 @@ export default function OpsRoute() {
135
136
  RetryDistillationResponse,
136
137
  RetryDistillationRequest
137
138
  >("retry-distillation" as any);
139
+ const healthQuery = useActionQuery<BrainHealthResponse>(
140
+ "get-brain-health" as any,
141
+ {} as any,
142
+ { refetchInterval: 30_000 },
143
+ );
144
+ const semantic = (
145
+ healthQuery.data as BrainHealthResponse & {
146
+ semanticIndex?: {
147
+ coverage?: number;
148
+ embeddingLag?: number;
149
+ aclFreshness?: number;
150
+ suppressed?: number;
151
+ };
152
+ }
153
+ )?.semanticIndex;
138
154
 
139
155
  const items = (queueQuery.data?.items ?? []) as BrainOpsQueueItemWithReason[];
140
156
  const summary = queueQuery.data?.summary ?? emptySummary;
@@ -377,6 +393,37 @@ export default function OpsRoute() {
377
393
  />
378
394
  </div>
379
395
 
396
+ <section className="grid gap-3 rounded-md border border-border bg-muted/20 p-4">
397
+ <div>
398
+ <p className="text-sm font-medium">{t("ops.semanticIndex")}</p>
399
+ <p className="mt-1 text-xs leading-5 text-muted-foreground">
400
+ {t("ops.semanticIndexDescription")}
401
+ </p>
402
+ </div>
403
+ <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
404
+ <MetricCard
405
+ label={t("ops.indexCoverage")}
406
+ value={semantic?.coverage ?? "—"}
407
+ detail={t("ops.adminOnly")}
408
+ />
409
+ <MetricCard
410
+ label={t("ops.embeddingLag")}
411
+ value={semantic?.embeddingLag ?? "—"}
412
+ detail={t("ops.adminOnly")}
413
+ />
414
+ <MetricCard
415
+ label={t("ops.aclFreshness")}
416
+ value={semantic?.aclFreshness ?? "—"}
417
+ detail={t("ops.adminOnly")}
418
+ />
419
+ <MetricCard
420
+ label={t("ops.suppressed")}
421
+ value={semantic?.suppressed ?? "—"}
422
+ detail={t("ops.adminOnly")}
423
+ />
424
+ </div>
425
+ </section>
426
+
380
427
  <div className="flex flex-col gap-3 rounded-md border border-border bg-card p-4 sm:flex-row sm:items-center sm:justify-between">
381
428
  <div className="min-w-0">
382
429
  <p className="text-sm font-medium">{t("ops.retryControls")}</p>
@@ -65,7 +65,7 @@ import { cn } from "@/lib/utils";
65
65
 
66
66
  type BrainT = ReturnType<typeof useT>;
67
67
 
68
- type ProposalStatus = "pending" | "approved" | "rejected";
68
+ type ProposalStatus = "pending" | "approved" | "rejected" | "quarantine";
69
69
 
70
70
  interface ProposalDraft {
71
71
  title?: string;
@@ -337,6 +337,9 @@ export default function ReviewRoute() {
337
337
  <SelectItem value="pending">{t("review.pending")}</SelectItem>
338
338
  <SelectItem value="approved">{t("review.approved")}</SelectItem>
339
339
  <SelectItem value="rejected">{t("review.rejected")}</SelectItem>
340
+ <SelectItem value="quarantine">
341
+ {t("review.quarantine")}
342
+ </SelectItem>
340
343
  </SelectContent>
341
344
  </Select>
342
345
  </div>
@@ -349,6 +352,11 @@ export default function ReviewRoute() {
349
352
  ) : proposals.length ? (
350
353
  <div className="grid gap-4">
351
354
  {proposals.map((proposal) => {
355
+ if (status === "quarantine") {
356
+ return (
357
+ <QuarantineMetadataCard key={proposal.id} item={proposal} />
358
+ );
359
+ }
352
360
  const evidence = proposal.evidence ?? [];
353
361
  const sourceUrl = firstSourceUrl(proposal);
354
362
  const canReview = proposal.status === "pending";
@@ -641,10 +649,41 @@ export default function ReviewRoute() {
641
649
  }
642
650
 
643
651
  function proposalStatus(value: string | null): ProposalStatus {
644
- if (value === "approved" || value === "rejected") return value;
652
+ if (value === "approved" || value === "rejected" || value === "quarantine")
653
+ return value;
645
654
  return "pending";
646
655
  }
647
656
 
657
+ function QuarantineMetadataCard({ item }: { item: ReviewItem }) {
658
+ const t = useT();
659
+ return (
660
+ <Card>
661
+ <CardHeader className="pb-3">
662
+ <div className="flex items-start justify-between gap-3">
663
+ <div className="min-w-0">
664
+ <CardTitle className="text-base">
665
+ {t("review.quarantineEvent")}
666
+ </CardTitle>
667
+ <p className="mt-1 text-sm text-muted-foreground">
668
+ {item.reason ?? t("review.quarantineNoReason")}
669
+ </p>
670
+ </div>
671
+ <Badge variant="outline">{t("review.metadataOnly")}</Badge>
672
+ </div>
673
+ </CardHeader>
674
+ <CardContent className="grid gap-2 text-sm text-muted-foreground sm:grid-cols-3">
675
+ <span>{t("review.quarantineId", { id: item.id })}</span>
676
+ <span>
677
+ {t("review.quarantineSource", {
678
+ source: item.sourceName ?? item.sourceId ?? t("review.notRecorded"),
679
+ })}
680
+ </span>
681
+ <span>{formatDate(item.createdAt, t)}</span>
682
+ </CardContent>
683
+ </Card>
684
+ );
685
+ }
686
+
648
687
  function cleanNote(value: string | undefined) {
649
688
  const trimmed = value?.trim();
650
689
  return trimmed ? trimmed : undefined;
@@ -42,6 +42,7 @@ import {
42
42
  SheetTrigger,
43
43
  } from "@/components/ui/sheet";
44
44
  import {
45
+ type BrainProjectsResponse,
45
46
  type KnowledgeRow,
46
47
  type SearchEverythingResponse,
47
48
  type SearchEverythingResult,
@@ -83,6 +84,7 @@ export default function SearchRoute() {
83
84
  const type = params.get("type") ?? "all";
84
85
  const provider = params.get("provider") ?? "all";
85
86
  const status = params.get("status") ?? "all";
87
+ const projectId = params.get("projectId") ?? "all";
86
88
  const limit = params.get("limit") ?? "25";
87
89
 
88
90
  const actionParams = useMemo(
@@ -90,10 +92,11 @@ export default function SearchRoute() {
90
92
  query: query.trim() || undefined,
91
93
  type: type === "all" ? undefined : type,
92
94
  provider: provider === "all" ? undefined : provider,
95
+ projectId: projectId === "all" ? undefined : projectId,
93
96
  status: status === "all" ? undefined : status,
94
97
  limit: Number.parseInt(limit, 10) || 25,
95
98
  }),
96
- [limit, provider, query, status, type],
99
+ [limit, projectId, provider, query, status, type],
97
100
  );
98
101
 
99
102
  const searchQuery = useActionQuery<SearchEverythingResponse>(
@@ -101,6 +104,10 @@ export default function SearchRoute() {
101
104
  actionParams as any,
102
105
  { enabled: Boolean(query.trim()), retry: false },
103
106
  );
107
+ const projectsQuery = useActionQuery<BrainProjectsResponse>(
108
+ "list-projects" as any,
109
+ {} as any,
110
+ );
104
111
 
105
112
  const results = useMemo(
106
113
  () => filterResults(normalizeResults(searchQuery.data), provider, status),
@@ -110,6 +117,7 @@ export default function SearchRoute() {
110
117
  Boolean(query.trim()) ||
111
118
  type !== "all" ||
112
119
  provider !== "all" ||
120
+ projectId !== "all" ||
113
121
  status !== "all";
114
122
  const groupedResults = useMemo(() => groupResults(results), [results]);
115
123
  const resultFacets = useMemo(
@@ -168,6 +176,24 @@ export default function SearchRoute() {
168
176
  </div>
169
177
 
170
178
  <div className="brain-search-filter-grid grid gap-2">
179
+ <Select
180
+ value={projectId}
181
+ onValueChange={(value) => updateParam("projectId", value)}
182
+ >
183
+ <SelectTrigger>
184
+ <SelectValue placeholder={t("searchPage.project")} />
185
+ </SelectTrigger>
186
+ <SelectContent>
187
+ <SelectItem value="all">
188
+ {t("searchPage.allProjects")}
189
+ </SelectItem>
190
+ {(projectsQuery.data?.projects ?? []).map((project) => (
191
+ <SelectItem key={project.id} value={project.id}>
192
+ {project.title}
193
+ </SelectItem>
194
+ ))}
195
+ </SelectContent>
196
+ </Select>
171
197
  <Select
172
198
  value={type}
173
199
  onValueChange={(value) => updateParam("type", value)}
@@ -346,6 +372,9 @@ function SearchResultRow({
346
372
  {displayLabel(sourceProvider)}
347
373
  </Badge>
348
374
  ) : null}
375
+ {result.matchLane ? (
376
+ <Badge variant="outline">{result.matchLane}</Badge>
377
+ ) : null}
349
378
  </div>
350
379
  <h3 className="mt-3 text-base font-semibold leading-6 text-foreground">
351
380
  {result.title || t("searchPage.untitledResult")}
@@ -356,7 +385,9 @@ function SearchResultRow({
356
385
  {whyMatched ? (
357
386
  <div className="mt-3 flex min-w-0 items-center gap-2 text-xs text-muted-foreground">
358
387
  <IconCircleCheck className="size-3.5 shrink-0 text-foreground" />
359
- <span className="truncate">{whyMatched}</span>
388
+ <span className="truncate">
389
+ {result.retrievalReason ?? whyMatched}
390
+ </span>
360
391
  </div>
361
392
  ) : null}
362
393
  </div>
@@ -16,6 +16,7 @@ import {
16
16
  IconDeviceFloppy,
17
17
  IconFileText,
18
18
  IconGauge,
19
+ IconLock,
19
20
  IconMessageCircle,
20
21
  IconShieldCheck,
21
22
  IconUsersGroup,
@@ -50,6 +51,7 @@ import { Textarea } from "@/components/ui/textarea";
50
51
  import {
51
52
  type BrainSettings,
52
53
  type SettingsResponse,
54
+ type BrainHealthResponse,
53
55
  defaultSettings,
54
56
  } from "@/lib/brain";
55
57
  import {
@@ -267,6 +269,152 @@ function PublishingReviewSettings({
267
269
  );
268
270
  }
269
271
 
272
+ function PrivacySensitivitySettings({
273
+ settings,
274
+ update,
275
+ }: {
276
+ settings: BrainSettings;
277
+ update: UpdateBrainSettings;
278
+ }) {
279
+ const t = useT();
280
+ const healthQuery = useActionQuery<BrainHealthResponse>(
281
+ "get-brain-health" as any,
282
+ {} as any,
283
+ );
284
+ const privacy = (
285
+ healthQuery.data as BrainHealthResponse & {
286
+ privacy?: {
287
+ classifierReady?: boolean;
288
+ classifierModel?: string | null;
289
+ quarantineRetentionDays?: number | null;
290
+ };
291
+ }
292
+ )?.privacy;
293
+ return (
294
+ <div className="mx-auto w-full max-w-3xl">
295
+ <Card id="privacy-sensitivity" className="scroll-mt-4">
296
+ <CardHeader>
297
+ <CardTitle className="flex items-center gap-2 text-base">
298
+ <IconLock className="size-4 text-primary" />
299
+ {t("settings.privacySensitivityTitle")}
300
+ </CardTitle>
301
+ <CardDescription>
302
+ {t("settings.privacySensitivityDescription")}
303
+ </CardDescription>
304
+ </CardHeader>
305
+ <CardContent className="grid gap-4 text-sm">
306
+ <div className="grid gap-3 rounded-md border border-border bg-muted/20 p-3 sm:grid-cols-3">
307
+ <PolicyRow
308
+ label={t("settings.privacyClassifier")}
309
+ value={
310
+ privacy?.classifierReady
311
+ ? t("settings.ready")
312
+ : t("settings.readinessPending")
313
+ }
314
+ />
315
+ <PolicyRow
316
+ label={t("settings.privacyModel")}
317
+ value={privacy?.classifierModel ?? t("settings.notSet")}
318
+ />
319
+ <PolicyRow
320
+ label={t("settings.quarantineRetention")}
321
+ value={
322
+ privacy?.quarantineRetentionDays
323
+ ? t("settings.days", {
324
+ count: privacy.quarantineRetentionDays,
325
+ })
326
+ : t("settings.notSet")
327
+ }
328
+ />
329
+ </div>
330
+ <p className="rounded-md border border-border bg-background p-3 text-xs leading-5 text-muted-foreground">
331
+ {t("settings.tightenOnly")}
332
+ </p>
333
+ <div className="grid gap-4 sm:grid-cols-2">
334
+ <TextField
335
+ id="privacy-classifier-model"
336
+ label={t("settings.privacyClassifierModel")}
337
+ value={settings.privacyClassifierModel ?? ""}
338
+ placeholder={t("settings.privacyClassifierModelPlaceholder")}
339
+ onChange={(value) => update("privacyClassifierModel", value)}
340
+ />
341
+ <TextField
342
+ id="privacy-classifier-engine"
343
+ label={t("settings.privacyClassifierEngine")}
344
+ value={settings.privacyClassifierEngine ?? ""}
345
+ placeholder={t("settings.privacyClassifierEnginePlaceholder")}
346
+ onChange={(value) => update("privacyClassifierEngine", value)}
347
+ />
348
+ </div>
349
+ <div className="grid gap-2">
350
+ <Label htmlFor="quarantine-retention-hours">
351
+ {t("settings.quarantineRetentionHours")}
352
+ </Label>
353
+ <Input
354
+ id="quarantine-retention-hours"
355
+ type="number"
356
+ min={1}
357
+ max={8760}
358
+ value={settings.quarantineRetentionHours ?? 72}
359
+ onChange={(event) =>
360
+ update(
361
+ "quarantineRetentionHours",
362
+ Math.max(1, Math.min(8760, Number(event.target.value) || 1)),
363
+ )
364
+ }
365
+ />
366
+ <p className="text-xs leading-5 text-muted-foreground">
367
+ {t("settings.quarantineRetentionHoursDescription")}
368
+ </p>
369
+ </div>
370
+ <div className="grid gap-2">
371
+ <Label htmlFor="sensitivity-custom-instructions">
372
+ {t("settings.sensitivityCustomInstructions")}
373
+ </Label>
374
+ <Textarea
375
+ id="sensitivity-custom-instructions"
376
+ value={settings.sensitivityCustomInstructions ?? ""}
377
+ placeholder={t(
378
+ "settings.sensitivityCustomInstructionsPlaceholder",
379
+ )}
380
+ onChange={(event) =>
381
+ update("sensitivityCustomInstructions", event.target.value)
382
+ }
383
+ />
384
+ <p className="text-xs leading-5 text-muted-foreground">
385
+ {t("settings.sensitivityCustomInstructionsDescription")}
386
+ </p>
387
+ </div>
388
+ <div className="grid gap-2">
389
+ <Label htmlFor="public-channel-exclusion-patterns">
390
+ {t("settings.publicChannelExclusionPatterns")}
391
+ </Label>
392
+ <Textarea
393
+ id="public-channel-exclusion-patterns"
394
+ value={(settings.publicChannelExclusionPatterns ?? []).join("\n")}
395
+ placeholder={t(
396
+ "settings.publicChannelExclusionPatternsPlaceholder",
397
+ )}
398
+ onChange={(event) =>
399
+ update(
400
+ "publicChannelExclusionPatterns",
401
+ event.target.value
402
+ .split("\n")
403
+ .map((value) => value.trim())
404
+ .filter(Boolean),
405
+ )
406
+ }
407
+ />
408
+ <p className="text-xs leading-5 text-muted-foreground">
409
+ {t("settings.publicChannelExclusionPatternsDescription")}
410
+ </p>
411
+ </div>
412
+ </CardContent>
413
+ </Card>
414
+ </div>
415
+ );
416
+ }
417
+
270
418
  function SafetyEvidenceSettings({
271
419
  settings,
272
420
  update,
@@ -399,6 +547,16 @@ export default function SettingsRoute() {
399
547
 
400
548
  const generalSearchEntries = useMemo<SettingsSearchEntry[]>(
401
549
  () => [
550
+ {
551
+ id: "privacy-sensitivity",
552
+ label: t("settings.privacySensitivityTitle"),
553
+ icon: IconLock,
554
+ keywords:
555
+ "privacy sensitivity classifier quarantine retention tighten only",
556
+ content: (
557
+ <PrivacySensitivitySettings settings={settings} update={update} />
558
+ ),
559
+ },
402
560
  {
403
561
  id: "brain-identity",
404
562
  label: t("settings.identityTitle"),
@@ -112,6 +112,7 @@ interface SourceFormState {
112
112
  sourceKey: string;
113
113
  autoSync: boolean;
114
114
  reviewRequired: boolean;
115
+ includePublicChannels: boolean;
115
116
  }
116
117
 
117
118
  const providers: Array<{
@@ -200,6 +201,7 @@ function defaultForm(
200
201
  autoSync:
201
202
  provider === "slack" || provider === "granola" || provider === "github",
202
203
  reviewRequired: true,
204
+ includePublicChannels: false,
203
205
  };
204
206
  }
205
207
 
@@ -251,6 +253,7 @@ function formFromSource(source: BrainSource): SourceFormState {
251
253
  sourceKey: "",
252
254
  autoSync: sourceAutoSync(source),
253
255
  reviewRequired: sourceReviewRequired(source),
256
+ includePublicChannels: config.includePublicChannels === true,
254
257
  };
255
258
  }
256
259
 
@@ -281,6 +284,7 @@ function buildConfig(form: SourceFormState) {
281
284
  if (form.provider === "slack") {
282
285
  config.channelIds = splitLines(form.channelRefs);
283
286
  config.historyLimit = numberValue(form.historyLimit, 15, 1, 15);
287
+ config.includePublicChannels = form.includePublicChannels;
284
288
  }
285
289
  if (form.provider === "granola") {
286
290
  config.pageSize = numberValue(form.granolaPageSize, 10, 1, 30);
@@ -2393,6 +2397,17 @@ export default function SourcesRoute() {
2393
2397
 
2394
2398
  {form.provider === "slack" && (
2395
2399
  <div className="grid gap-4 rounded-md border border-border p-4">
2400
+ <div className="grid gap-3 rounded-md border border-border bg-muted/20 p-3 text-xs leading-5 text-muted-foreground">
2401
+ <p>
2402
+ <span className="font-medium text-foreground">
2403
+ {t("sources.slackDiscoveryMode")}
2404
+ </span>{" "}
2405
+ — {t("sources.slackDiscoveryModeDescription")}
2406
+ </p>
2407
+ <p>{t("sources.slackExclusions")}</p>
2408
+ <p>{t("sources.slackManualInvite")}</p>
2409
+ <p>{t("sources.sensitivityPreviewDescription")}</p>
2410
+ </div>
2396
2411
  <div className="rounded-md border border-border bg-muted/25 p-3">
2397
2412
  <div className="flex items-center gap-2 text-sm font-medium">
2398
2413
  <IconShieldCheck className="size-4 text-muted-foreground" />
@@ -2419,6 +2434,22 @@ export default function SourcesRoute() {
2419
2434
  {t("sources.allowedChannelsDescription")}
2420
2435
  </p>
2421
2436
  </div>
2437
+ <label className="flex items-center justify-between gap-4 rounded-md border border-border bg-muted/20 p-3">
2438
+ <span>
2439
+ <span className="block text-sm font-medium">
2440
+ {t("sources.includePublicChannels")}
2441
+ </span>
2442
+ <span className="mt-1 block text-xs leading-5 text-muted-foreground">
2443
+ {t("sources.includePublicChannelsDescription")}
2444
+ </span>
2445
+ </span>
2446
+ <Switch
2447
+ checked={form.includePublicChannels}
2448
+ onCheckedChange={(includePublicChannels) =>
2449
+ updateForm({ includePublicChannels })
2450
+ }
2451
+ />
2452
+ </label>
2422
2453
  <div className="grid gap-2 sm:grid-cols-2">
2423
2454
  <div className="grid gap-2">
2424
2455
  <Label htmlFor="history-limit">
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-19
4
+ ---
5
+
6
+ Brain now searches approved public, private, and meeting knowledge semantically while keeping sensitive material out of retrieval.