claude-smart 0.2.49 → 0.2.50

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 (177) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.md +10 -43
  3. package/bin/claude-smart.js +105 -0
  4. package/package.json +3 -2
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/.coverage +0 -0
  8. package/plugin/README.md +4 -3
  9. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +4 -2
  10. package/plugin/dashboard/app/dashboard/page.tsx +6 -1
  11. package/plugin/dashboard/app/preferences/[id]/page.tsx +18 -6
  12. package/plugin/dashboard/app/preferences/page.tsx +32 -35
  13. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +16 -1
  14. package/plugin/dashboard/app/sessions/page.tsx +2 -0
  15. package/plugin/dashboard/app/skills/page.tsx +65 -50
  16. package/plugin/dashboard/app/skills/project/[id]/page.tsx +17 -8
  17. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +1 -6
  18. package/plugin/dashboard/components/common/host-badge.tsx +118 -0
  19. package/plugin/dashboard/components/common/learning-application-badge.tsx +34 -0
  20. package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
  21. package/plugin/dashboard/components/common/page-header.tsx +3 -3
  22. package/plugin/dashboard/lib/config-file.ts +5 -1
  23. package/plugin/dashboard/lib/host-attribution.ts +62 -0
  24. package/plugin/dashboard/lib/session-reader.ts +40 -2
  25. package/plugin/dashboard/lib/types.ts +7 -1
  26. package/plugin/pyproject.toml +1 -1
  27. package/plugin/scripts/_lib.sh +197 -1
  28. package/plugin/scripts/backend-python-runner.py +46 -0
  29. package/plugin/scripts/backend-service.sh +757 -119
  30. package/plugin/scripts/codex-hook.js +63 -225
  31. package/plugin/scripts/dashboard-open.sh +6 -4
  32. package/plugin/scripts/dashboard-service.sh +117 -136
  33. package/plugin/scripts/hook_entry.sh +3 -0
  34. package/plugin/scripts/smart-install.sh +15 -1
  35. package/plugin/src/claude_smart/cli.py +14 -0
  36. package/plugin/src/claude_smart/context_inject.py +3 -0
  37. package/plugin/src/claude_smart/env_config.py +4 -1
  38. package/plugin/src/claude_smart/events/post_tool.py +2 -1
  39. package/plugin/src/claude_smart/events/session_end.py +2 -1
  40. package/plugin/src/claude_smart/events/stop.py +3 -0
  41. package/plugin/src/claude_smart/events/user_prompt.py +2 -1
  42. package/plugin/src/claude_smart/internal_call.py +5 -2
  43. package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
  44. package/plugin/src/claude_smart/publish.py +59 -7
  45. package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
  46. package/plugin/src/claude_smart/runtime.py +15 -6
  47. package/plugin/src/claude_smart/state.py +211 -52
  48. package/plugin/uv.lock +1 -1
  49. package/plugin/vendor/reflexio/.env.example +13 -0
  50. package/plugin/vendor/reflexio/reflexio/README.md +7 -3
  51. package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
  52. package/plugin/vendor/reflexio/reflexio/client/client.py +126 -3
  53. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
  54. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
  55. package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
  56. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +177 -25
  57. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
  58. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
  59. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
  60. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
  61. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
  62. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
  63. package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
  64. package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
  65. package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
  66. package/plugin/vendor/reflexio/reflexio/server/api.py +66 -3
  67. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
  68. package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
  69. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
  70. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
  71. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +62 -5
  72. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
  73. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
  74. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
  75. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
  76. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
  77. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +20 -5
  78. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
  79. package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
  80. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
  81. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
  82. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
  83. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
  84. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
  85. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
  86. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
  87. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
  88. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
  89. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
  90. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
  91. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
  92. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
  93. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
  94. package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
  95. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
  96. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
  97. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
  98. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
  99. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
  100. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
  101. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
  102. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
  103. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
  104. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
  105. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
  106. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
  107. package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +85 -16
  109. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
  110. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
  111. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
  112. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
  113. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
  114. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
  115. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
  116. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
  117. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +510 -63
  118. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
  120. package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
  121. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
  122. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
  124. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
  125. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
  126. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
  127. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
  128. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
  129. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
  130. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
  131. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
  132. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
  133. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +122 -44
  134. package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
  135. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
  138. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
  139. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
  140. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
  141. package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
  142. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
  143. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
  144. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +1 -1
  145. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +50 -0
  146. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
  147. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
  148. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +36 -1
  149. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
  150. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +43 -0
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +45 -7
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +146 -6
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +42 -7
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +1 -1
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +50 -0
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
  169. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +59 -0
  170. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +52 -2
  171. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
  172. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
  173. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
  174. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
  175. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
  176. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
  177. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
@@ -2,8 +2,15 @@
2
2
 
3
3
  import { useEffect, useMemo, useState } from "react";
4
4
  import Link from "next/link";
5
- import { BookOpen, ChevronRight, Layers3 } from "lucide-react";
5
+ import {
6
+ AlertTriangle,
7
+ BookOpen,
8
+ ChevronRight,
9
+ Layers3,
10
+ } from "lucide-react";
6
11
  import { PageHeader } from "@/components/common/page-header";
12
+ import { HostBadge } from "@/components/common/host-badge";
13
+ import { LearningApplicationBadge } from "@/components/common/learning-application-badge";
7
14
  import { EmptyState } from "@/components/common/empty-state";
8
15
  import { DeleteAllButton } from "@/components/common/delete-all-button";
9
16
  import { PageTabs } from "@/components/common/page-tabs";
@@ -18,6 +25,7 @@ import {
18
25
  } from "@/components/ui/select";
19
26
  import { reflexio } from "@/lib/reflexio-client";
20
27
  import { formatRelative } from "@/lib/format";
28
+ import { useRequestHostAttribution } from "@/lib/host-attribution";
21
29
  import { cn } from "@/lib/utils";
22
30
  import {
23
31
  agentPlaybookStatusLabel,
@@ -27,6 +35,7 @@ import {
27
35
  } from "@/lib/status";
28
36
  import type {
29
37
  AgentPlaybook,
38
+ Host,
30
39
  PlaybookApplicationStat,
31
40
  UserPlaybook,
32
41
  } from "@/lib/types";
@@ -58,24 +67,29 @@ const SHARED_STATUS_META: Record<
58
67
  interface SkillCard {
59
68
  kind: SkillKind;
60
69
  id: number;
61
- agentVersion: string;
62
70
  createdAt: number;
63
71
  content: string;
64
72
  trigger: string | null;
65
73
  rationale: string | null;
66
74
  status: SkillStatus;
75
+ scopeId: string;
76
+ host: Host | null;
67
77
  }
68
78
 
69
- function projectSkill(p: UserPlaybook): SkillCard {
79
+ function projectSkill(
80
+ p: UserPlaybook,
81
+ requestHosts?: ReadonlyMap<string, Host | null>,
82
+ ): SkillCard {
70
83
  return {
71
84
  kind: "project",
72
85
  id: p.user_playbook_id,
73
- agentVersion: p.agent_version || "default",
74
86
  createdAt: p.created_at,
75
87
  content: p.content,
76
88
  trigger: p.trigger,
77
89
  rationale: p.rationale,
78
90
  status: statusLabel(p),
91
+ scopeId: p.user_id || "unknown",
92
+ host: requestHosts?.get(p.request_id) ?? null,
79
93
  };
80
94
  }
81
95
 
@@ -83,12 +97,13 @@ function sharedSkill(p: AgentPlaybook): SkillCard {
83
97
  return {
84
98
  kind: "shared",
85
99
  id: p.agent_playbook_id,
86
- agentVersion: p.agent_version || "default",
87
100
  createdAt: p.created_at,
88
101
  content: p.content,
89
102
  trigger: p.trigger,
90
103
  rationale: p.rationale,
91
104
  status: agentPlaybookStatusLabel(p),
105
+ scopeId: p.agent_version || "default",
106
+ host: null,
92
107
  };
93
108
  }
94
109
 
@@ -106,10 +121,11 @@ export default function SkillsPage() {
106
121
  );
107
122
  const [error, setError] = useState<string | null>(null);
108
123
  const [activeKind, setActiveKind] = useState<SkillKind>("project");
109
- const [agentVersion, setAgentVersion] = useState<string>("__all__");
124
+ const [scope, setScope] = useState<string>("__all__");
110
125
  const [statusFilter, setStatusFilter] = useState<string>("CURRENT");
111
126
  const [sortBy, setSortBy] = useState<SkillSort>("newest");
112
127
  const [search, setSearch] = useState("");
128
+ const attribution = useRequestHostAttribution();
113
129
 
114
130
  useEffect(() => {
115
131
  let cancelled = false;
@@ -158,24 +174,26 @@ export default function SkillsPage() {
158
174
 
159
175
  const activeSkills = useMemo(() => {
160
176
  return activeKind === "project"
161
- ? (projectSkills ?? []).map(projectSkill)
177
+ ? (projectSkills ?? []).map((playbook) =>
178
+ projectSkill(playbook, attribution?.hosts),
179
+ )
162
180
  : (sharedSkills ?? []).map(sharedSkill);
163
- }, [activeKind, projectSkills, sharedSkills]);
181
+ }, [activeKind, attribution, projectSkills, sharedSkills]);
164
182
 
165
- const projects = useMemo(() => {
183
+ const scopes = useMemo(() => {
166
184
  const set = new Set<string>();
167
- for (const p of activeSkills) set.add(p.agentVersion);
185
+ for (const p of activeSkills) set.add(p.scopeId);
168
186
  return Array.from(set).sort();
169
187
  }, [activeSkills]);
170
188
 
171
189
  const filtered = useMemo(() => {
172
190
  const matches = activeSkills.filter((p) => {
173
- if (agentVersion !== "__all__" && p.agentVersion !== agentVersion)
191
+ if (scope !== "__all__" && p.scopeId !== scope)
174
192
  return false;
175
193
  if (statusFilter !== "__all__" && p.status !== statusFilter) return false;
176
194
  if (search) {
177
195
  const s = search.toLowerCase();
178
- const hay = `${p.content} ${p.trigger ?? ""} ${p.rationale ?? ""}`.toLowerCase();
196
+ const hay = `${p.scopeId} ${p.content} ${p.trigger ?? ""} ${p.rationale ?? ""}`.toLowerCase();
179
197
  if (!hay.includes(s)) return false;
180
198
  }
181
199
  return true;
@@ -193,7 +211,7 @@ export default function SkillsPage() {
193
211
  }
194
212
  return b.createdAt - a.createdAt;
195
213
  });
196
- }, [activeSkills, agentVersion, search, sortBy, statsByRule, statusFilter]);
214
+ }, [activeSkills, scope, search, sortBy, statsByRule, statusFilter]);
197
215
 
198
216
  const projectCount = projectSkills?.length ?? 0;
199
217
  const sharedCount = sharedSkills?.length ?? 0;
@@ -201,10 +219,12 @@ export default function SkillsPage() {
201
219
  const activeCount = activeKind === "project" ? projectCount : sharedCount;
202
220
  const loading = projectSkills === null || sharedSkills === null;
203
221
  const hasNoSharedSkills = activeKind === "shared" && sharedCount === 0;
222
+ const allScopesLabel =
223
+ activeKind === "project" ? "All user scopes" : "All agents";
204
224
 
205
225
  const switchKind = (kind: SkillKind) => {
206
226
  setActiveKind(kind);
207
- setAgentVersion("__all__");
227
+ setScope("__all__");
208
228
  setStatusFilter(kind === "project" ? "CURRENT" : "__all__");
209
229
  };
210
230
 
@@ -216,15 +236,17 @@ export default function SkillsPage() {
216
236
  actions={
217
237
  <div className="flex max-w-full flex-wrap items-center justify-end gap-2">
218
238
  <Select
219
- value={agentVersion}
220
- onValueChange={(v) => setAgentVersion(v ?? "__all__")}
239
+ value={scope}
240
+ onValueChange={(v) => setScope(v ?? "__all__")}
221
241
  >
222
242
  <SelectTrigger size="sm" className="w-40 text-xs bg-background/80">
223
- <SelectValue placeholder="Project" />
243
+ <SelectValue>
244
+ {scope === "__all__" ? allScopesLabel : scope}
245
+ </SelectValue>
224
246
  </SelectTrigger>
225
247
  <SelectContent>
226
- <SelectItem value="__all__">All projects</SelectItem>
227
- {projects.map((p) => (
248
+ <SelectItem value="__all__">{allScopesLabel}</SelectItem>
249
+ {scopes.map((p) => (
228
250
  <SelectItem key={p} value={p}>
229
251
  {p}
230
252
  </SelectItem>
@@ -337,6 +359,13 @@ export default function SkillsPage() {
337
359
  {error}. Is reflexio running on the configured backend URL?
338
360
  </div>
339
361
  )}
362
+ {activeKind === "project" && attribution?.unavailable && !error && (
363
+ <div className="mb-4 flex items-center gap-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm text-amber-800 dark:text-amber-200">
364
+ <AlertTriangle className="h-4 w-4 shrink-0" />
365
+ Skill source details are temporarily unavailable. Skills remain
366
+ available.
367
+ </div>
368
+ )}
340
369
 
341
370
  {loading && !error ? (
342
371
  <div className="text-sm text-muted-foreground">Loading...</div>
@@ -366,17 +395,20 @@ export default function SkillsPage() {
366
395
  >
367
396
  <header className="mb-2 flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
368
397
  <div className="flex min-w-0 flex-wrap items-center gap-2">
369
- <Badge
370
- variant="outline"
371
- className="h-5 max-w-56 truncate font-mono text-[10px]"
372
- >
373
- {p.agentVersion}
374
- </Badge>
398
+ <LearningApplicationBadge stat={stat} />
375
399
  <StatusBadge kind={p.kind} status={p.status} />
376
400
  <Badge variant="secondary" className="h-5 text-[10px]">
377
401
  {p.kind === "project" ? "project-specific" : "shared"}
378
402
  </Badge>
379
- <ApplicationStatBadge stat={stat} />
403
+ {p.kind === "shared" && (
404
+ <Badge
405
+ variant="outline"
406
+ className="h-5 max-w-56 truncate font-mono text-[10px]"
407
+ title="Shared-skill applicability scope; not the source host."
408
+ >
409
+ Agent scope: {p.scopeId}
410
+ </Badge>
411
+ )}
380
412
  </div>
381
413
  <div className="flex shrink-0 items-center gap-1.5 pt-0.5">
382
414
  <span className="text-[11px] text-muted-foreground">
@@ -406,6 +438,13 @@ export default function SkillsPage() {
406
438
  {p.rationale}
407
439
  </p>
408
440
  )}
441
+ {p.kind === "project" &&
442
+ attribution &&
443
+ !attribution.unavailable && (
444
+ <div className="mt-2">
445
+ <HostBadge host={p.host} display="provenance" />
446
+ </div>
447
+ )}
409
448
  </Link>
410
449
  );
411
450
  })}
@@ -416,30 +455,6 @@ export default function SkillsPage() {
416
455
  );
417
456
  }
418
457
 
419
- function ApplicationStatBadge({ stat }: { stat: PlaybookApplicationStat | undefined }) {
420
- if (!stat || stat.applied_count === 0) {
421
- return (
422
- <Badge
423
- variant="outline"
424
- className="h-5 text-[10px] text-muted-foreground"
425
- title="No citations recorded yet for this rule. It will count once an assistant reply cites it."
426
- >
427
- Never applied
428
- </Badge>
429
- );
430
- }
431
- const last = formatRelative(stat.last_applied_at);
432
- return (
433
- <Badge
434
- variant="secondary"
435
- className="h-5 text-[10px]"
436
- title={`Last applied ${last}`}
437
- >
438
- Applied {stat.applied_count}×{stat.last_applied_at ? ` · ${last}` : ""}
439
- </Badge>
440
- );
441
- }
442
-
443
458
  function statusFilterLabel(kind: SkillKind, status: string): string {
444
459
  if (status === "__all__") return "All";
445
460
  if (kind === "shared") {
@@ -13,11 +13,11 @@ import {
13
13
  Check,
14
14
  BookMarked,
15
15
  Hash,
16
- FolderGit2,
17
16
  Clock,
18
17
  FileText,
19
18
  } from "lucide-react";
20
19
  import { PageHeader } from "@/components/common/page-header";
20
+ import { LearningHostProvenance } from "@/components/common/host-badge";
21
21
  import { EmptyState } from "@/components/common/empty-state";
22
22
  import { DeleteLearningDangerZone } from "@/components/common/delete-learning-danger-zone";
23
23
  import { Button } from "@/components/ui/button";
@@ -26,6 +26,7 @@ import { Badge } from "@/components/ui/badge";
26
26
  import { Separator } from "@/components/ui/separator";
27
27
  import { reflexio } from "@/lib/reflexio-client";
28
28
  import { formatTimestamp, truncateId } from "@/lib/format";
29
+ import { useRequestHostAttribution } from "@/lib/host-attribution";
29
30
  import { cn } from "@/lib/utils";
30
31
  import { statusLabel } from "@/lib/status";
31
32
  import type { StatusLabel } from "@/lib/status";
@@ -66,6 +67,7 @@ export default function ProjectSkillDetailPage({
66
67
  trigger: "",
67
68
  rationale: "",
68
69
  });
70
+ const attribution = useRequestHostAttribution();
69
71
 
70
72
  useEffect(() => {
71
73
  let cancelled = false;
@@ -229,10 +231,6 @@ export default function ProjectSkillDetailPage({
229
231
 
230
232
  {playbook && (
231
233
  <div className="flex items-center gap-2 flex-wrap">
232
- <Badge variant="outline" className="gap-1.5">
233
- <FolderGit2 className="h-3 w-3" />
234
- {playbook.agent_version || "default"}
235
- </Badge>
236
234
  <StatusBadge status={status!} />
237
235
  {displayName(playbook.playbook_name) && (
238
236
  <Badge variant="secondary" className="font-mono text-[10px]">
@@ -332,7 +330,7 @@ export default function ProjectSkillDetailPage({
332
330
  value={formatTimestamp(playbook.created_at)}
333
331
  />
334
332
  <Meta
335
- label="Project"
333
+ label="Agent version"
336
334
  value={playbook.agent_version || "default"}
337
335
  mono
338
336
  />
@@ -350,8 +348,19 @@ export default function ProjectSkillDetailPage({
350
348
  display={truncateId(playbook.request_id, 8, 4)}
351
349
  />
352
350
  )}
351
+ {attribution && playbook.request_id && (
352
+ <Meta
353
+ label="Origin"
354
+ value={
355
+ <LearningHostProvenance
356
+ attribution={attribution}
357
+ requestId={playbook.request_id}
358
+ />
359
+ }
360
+ />
361
+ )}
353
362
  {playbook.source && (
354
- <Meta label="Source" value={playbook.source} mono />
363
+ <Meta label="Integration" value={playbook.source} mono />
355
364
  )}
356
365
  </dl>
357
366
  </div>
@@ -499,7 +508,7 @@ function Meta({
499
508
  }: {
500
509
  icon?: React.ComponentType<{ className?: string }>;
501
510
  label: string;
502
- value: string;
511
+ value: React.ReactNode;
503
512
  mono?: boolean;
504
513
  }) {
505
514
  return (
@@ -13,7 +13,6 @@ import {
13
13
  Check,
14
14
  BookMarked,
15
15
  Hash,
16
- FolderGit2,
17
16
  Clock,
18
17
  FileText,
19
18
  } from "lucide-react";
@@ -298,10 +297,6 @@ export default function SharedSkillDetailPage({
298
297
 
299
298
  {playbook && (
300
299
  <div className="flex items-center gap-2 flex-wrap">
301
- <Badge variant="outline" className="gap-1.5">
302
- <FolderGit2 className="h-3 w-3" />
303
- {playbook.agent_version || "default"}
304
- </Badge>
305
300
  {editing ? (
306
301
  <ReviewStatusBadge
307
302
  status={playbook.playbook_status}
@@ -472,7 +467,7 @@ export default function SharedSkillDetailPage({
472
467
  value={formatTimestamp(playbook.created_at)}
473
468
  />
474
469
  <Meta
475
- label="Project"
470
+ label="Agent version"
476
471
  value={playbook.agent_version || "default"}
477
472
  mono
478
473
  />
@@ -0,0 +1,118 @@
1
+ import { Terminal } from "lucide-react";
2
+ import { Badge } from "@/components/ui/badge";
3
+ import type { RequestHostAttribution } from "@/lib/host-attribution";
4
+ import { cn } from "@/lib/utils";
5
+ import type { Host } from "@/lib/types";
6
+
7
+ // Exact product colors from the current official app assets. Label colors meet
8
+ // WCAG AA for the badge's small text: Claude 5.65:1, Codex 5.39:1, OpenCode 18.93:1.
9
+ const HOST_META: Record<
10
+ Exclude<Host, "unknown">,
11
+ { label: string; badgeClass: string; dotClass: string }
12
+ > = {
13
+ "claude-code": {
14
+ label: "Claude Code",
15
+ badgeClass: "border-[#D97757] bg-[#D97757] text-[#2A120B]",
16
+ dotClass: "bg-[#D97757]",
17
+ },
18
+ codex: {
19
+ label: "Codex",
20
+ badgeClass: "border-[#0169CC] bg-[#0169CC] text-white",
21
+ dotClass: "bg-[#0169CC]",
22
+ },
23
+ opencode: {
24
+ label: "OpenCode",
25
+ badgeClass:
26
+ "border-[#131010] bg-[#131010] text-white dark:border-white/30",
27
+ dotClass: "bg-[#131010] dark:bg-white",
28
+ },
29
+ };
30
+
31
+ export function HostBadge({
32
+ host,
33
+ display = "badge",
34
+ size = "md",
35
+ className,
36
+ }: {
37
+ host: Host | null;
38
+ display?: "badge" | "provenance";
39
+ size?: "md" | "sm";
40
+ className?: string;
41
+ }) {
42
+ const knownHost = host && host !== "unknown" ? host : null;
43
+ const meta = knownHost ? HOST_META[knownHost] : null;
44
+ const label = meta?.label ?? "unknown host";
45
+
46
+ if (display === "provenance") {
47
+ return (
48
+ <span
49
+ className={cn(
50
+ "inline-flex items-center gap-1 text-[11px] text-muted-foreground",
51
+ className,
52
+ )}
53
+ title={
54
+ knownHost
55
+ ? `This learning was generated from a ${label} session; it is not limited to that host.`
56
+ : "The session host for this learning's generation request was not recorded."
57
+ }
58
+ >
59
+ <span>Generated via</span>
60
+ <span
61
+ aria-hidden="true"
62
+ className={cn(
63
+ "h-1.5 w-1.5 shrink-0 rounded-full",
64
+ meta
65
+ ? meta.dotClass
66
+ : "border border-dashed border-muted-foreground/70",
67
+ )}
68
+ />
69
+ <span className="font-medium text-foreground/75">{label}</span>
70
+ </span>
71
+ );
72
+ }
73
+
74
+ const compact = size === "sm";
75
+ const sizeClass = compact ? "h-4 px-1.5 text-[10px]" : "h-5";
76
+ const badgeLabel = knownHost ? label : "Host unknown";
77
+ return (
78
+ <Badge
79
+ variant="outline"
80
+ className={cn(
81
+ "gap-1",
82
+ meta
83
+ ? meta.badgeClass
84
+ : "border-dashed text-muted-foreground",
85
+ sizeClass,
86
+ className,
87
+ )}
88
+ title={knownHost ? `Session host: ${label}` : "Session host was not recorded"}
89
+ >
90
+ <Terminal
91
+ className={cn(
92
+ knownHost ? "text-current" : "text-muted-foreground",
93
+ compact ? "h-2.5 w-2.5" : "h-3 w-3",
94
+ )}
95
+ />
96
+ {badgeLabel}
97
+ </Badge>
98
+ );
99
+ }
100
+
101
+ export function LearningHostProvenance({
102
+ attribution,
103
+ requestId,
104
+ }: {
105
+ attribution: RequestHostAttribution;
106
+ requestId: string;
107
+ }) {
108
+ if (attribution.unavailable) {
109
+ return <span className="text-muted-foreground">Unavailable</span>;
110
+ }
111
+
112
+ return (
113
+ <HostBadge
114
+ host={attribution.hosts.get(requestId) ?? null}
115
+ display="provenance"
116
+ />
117
+ );
118
+ }
@@ -0,0 +1,34 @@
1
+ import { Sparkles } from "lucide-react";
2
+ import { Badge } from "@/components/ui/badge";
3
+ import { formatRelative } from "@/lib/format";
4
+ import type { PlaybookApplicationStat } from "@/lib/types";
5
+
6
+ export function LearningApplicationBadge({
7
+ stat,
8
+ }: {
9
+ stat: PlaybookApplicationStat | undefined;
10
+ }) {
11
+ if (!stat || stat.applied_count === 0) {
12
+ return (
13
+ <Badge
14
+ variant="outline"
15
+ className="h-5 text-[10px] text-muted-foreground"
16
+ title="No citations recorded yet for this learning. It will count once an assistant reply cites it."
17
+ >
18
+ Never applied
19
+ </Badge>
20
+ );
21
+ }
22
+
23
+ const last = formatRelative(stat.last_applied_at);
24
+ return (
25
+ <Badge
26
+ variant="outline"
27
+ className="h-5 gap-1 border-amber-500/45 bg-amber-500/10 text-[10px] text-amber-700 dark:text-amber-300"
28
+ title={`Last applied ${last}`}
29
+ >
30
+ <Sparkles className="h-2.5 w-2.5 text-amber-500" />
31
+ Applied {stat.applied_count}×{stat.last_applied_at ? ` · ${last}` : ""}
32
+ </Badge>
33
+ );
34
+ }
@@ -28,7 +28,7 @@ export function LearningsBadge({
28
28
  isSmall ? "h-2.5 w-2.5" : "h-3 w-3",
29
29
  )}
30
30
  />
31
- {count} learning applied
31
+ {count} learning{count === 1 ? "" : "s"} applied
32
32
  </Badge>
33
33
  );
34
34
  }
@@ -7,7 +7,7 @@ export function PageHeader({
7
7
  className,
8
8
  }: {
9
9
  title: string;
10
- description?: string;
10
+ description?: React.ReactNode;
11
11
  actions?: React.ReactNode;
12
12
  className?: string;
13
13
  }) {
@@ -21,9 +21,9 @@ export function PageHeader({
21
21
  <div className="min-w-[min(18rem,100%)] flex-1">
22
22
  <h1 className="text-2xl font-semibold">{title}</h1>
23
23
  {description && (
24
- <p className="text-sm text-muted-foreground mt-1 max-w-3xl">
24
+ <div className="text-sm text-muted-foreground mt-1 max-w-3xl">
25
25
  {description}
26
- </p>
26
+ </div>
27
27
  )}
28
28
  </div>
29
29
  {actions && (
@@ -27,6 +27,10 @@ const BOOL_KEYS = new Set([
27
27
  "CLAUDE_SMART_READ_ONLY",
28
28
  ]);
29
29
 
30
+ function defaultReflexioUrl(): string {
31
+ return `http://localhost:${process.env.BACKEND_PORT || "8071"}/`;
32
+ }
33
+
30
34
  function envPath(): string {
31
35
  return path.join(os.homedir(), ".reflexio", ".env");
32
36
  }
@@ -49,7 +53,7 @@ function parseLine(line: string): { key: string; value: string } | null {
49
53
 
50
54
  export async function readConfig(): Promise<ClaudeSmartConfig> {
51
55
  const defaults: ClaudeSmartConfig = {
52
- REFLEXIO_URL: "http://localhost:8071/",
56
+ REFLEXIO_URL: defaultReflexioUrl(),
53
57
  REFLEXIO_API_KEY: "",
54
58
  CLAUDE_SMART_USE_LOCAL_CLI: false,
55
59
  CLAUDE_SMART_USE_LOCAL_EMBEDDING: false,
@@ -0,0 +1,62 @@
1
+ import { useEffect, useState } from "react";
2
+ import type { Host, SessionSummary } from "./types";
3
+
4
+ export interface RequestHostAttribution {
5
+ hosts: ReadonlyMap<string, Host | null>;
6
+ unavailable: boolean;
7
+ }
8
+
9
+ function hostsByRequestId(
10
+ sessions: SessionSummary[],
11
+ ): Map<string, Host | null> {
12
+ const result = new Map<string, Host | null>();
13
+ for (const session of sessions) {
14
+ for (const requestId of session.request_ids) {
15
+ result.set(requestId, session.host);
16
+ }
17
+ }
18
+ return result;
19
+ }
20
+
21
+ export function useRequestHostAttribution(): RequestHostAttribution | null {
22
+ const [attribution, setAttribution] =
23
+ useState<RequestHostAttribution | null>(null);
24
+
25
+ useEffect(() => {
26
+ let cancelled = false;
27
+ const controller = new AbortController();
28
+ const timeout = setTimeout(() => controller.abort(), 5_000);
29
+
30
+ fetch("/api/sessions", {
31
+ cache: "no-store",
32
+ signal: controller.signal,
33
+ })
34
+ .then(async (response) => {
35
+ if (!response.ok) throw new Error(`sessions ${response.status}`);
36
+ const data = await response.json();
37
+ return (data.sessions ?? []) as SessionSummary[];
38
+ })
39
+ .then((sessions) => {
40
+ if (!cancelled) {
41
+ setAttribution({
42
+ hosts: hostsByRequestId(sessions),
43
+ unavailable: false,
44
+ });
45
+ }
46
+ })
47
+ .catch(() => {
48
+ if (!cancelled) {
49
+ setAttribution({ hosts: new Map(), unavailable: true });
50
+ }
51
+ })
52
+ .finally(() => clearTimeout(timeout));
53
+
54
+ return () => {
55
+ cancelled = true;
56
+ controller.abort();
57
+ clearTimeout(timeout);
58
+ };
59
+ }, []);
60
+
61
+ return attribution;
62
+ }