arkaos 2.85.0 → 2.87.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.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 2.85.0
1
+ 2.87.0
@@ -2,12 +2,73 @@
2
2
  import type { TableColumn } from '@nuxt/ui'
3
3
  import type { Agent } from '~/types'
4
4
 
5
- const { fetchApi } = useApi()
5
+ const { fetchApi, apiBase } = useApi()
6
+ const toast = useToast()
6
7
 
7
8
  const { data, status, error, refresh } = await fetchApi<{ agents: Agent[], total: number }>('/api/agents')
8
9
 
10
+ // PR69 v2.86.0 — per-department activity from PR47 telemetry.
11
+ // Used to badge agents whose department has run recently and to
12
+ // surface "no activity yet" hint when a department's never been
13
+ // invoked. Failure-tolerant — returns empty if telemetry unavailable.
14
+ interface ActivityRow {
15
+ call_count: number
16
+ total_cost_usd: number | null
17
+ total_tokens_in: number
18
+ total_tokens_out: number
19
+ }
20
+
21
+ const {
22
+ data: activityData,
23
+ refresh: refreshActivity,
24
+ } = fetchApi<{ by_department: Record<string, ActivityRow>, period: string }>(
25
+ '/api/agents/activity?period=week',
26
+ )
27
+
9
28
  const agents = computed(() => data.value?.agents ?? [])
10
29
 
30
+ function deptActivity(dept: string): ActivityRow | undefined {
31
+ return activityData.value?.by_department?.[dept]
32
+ }
33
+
34
+ const copied = ref<string | null>(null)
35
+ let copyTimer: ReturnType<typeof setTimeout> | null = null
36
+
37
+ async function copyAgentMention(agent: Agent) {
38
+ if (typeof navigator === 'undefined' || !navigator.clipboard) {
39
+ toast.add({ title: 'Clipboard unavailable', color: 'warning' })
40
+ return
41
+ }
42
+ // The most useful copy for an operator: a ready-to-paste mention
43
+ // that names the agent + their role so the orchestrator can dispatch.
44
+ const text = `Use ${agent.name} (${agent.role}, dept ${agent.department}, tier ${agent.tier}) for this task.`
45
+ try {
46
+ await navigator.clipboard.writeText(text)
47
+ copied.value = agent.id
48
+ if (copyTimer) clearTimeout(copyTimer)
49
+ copyTimer = setTimeout(() => { copied.value = null; copyTimer = null }, 1500)
50
+ toast.add({
51
+ title: 'Copied',
52
+ description: `${agent.name} mention ready to paste.`,
53
+ color: 'success',
54
+ })
55
+ } catch (err) {
56
+ toast.add({
57
+ title: 'Copy failed',
58
+ description: err instanceof Error ? err.message : 'unknown error',
59
+ color: 'error',
60
+ })
61
+ }
62
+ }
63
+
64
+ onBeforeUnmount(() => {
65
+ if (copyTimer) clearTimeout(copyTimer)
66
+ })
67
+
68
+ async function refreshAll() {
69
+ await Promise.all([refresh(), refreshActivity()])
70
+ }
71
+
11
72
  const search = ref('')
12
73
  const departmentFilter = ref('all')
13
74
  const tierFilter = ref('all')
@@ -77,35 +138,18 @@ const tierColor = (tier: number) => {
77
138
  }
78
139
 
79
140
  const columns: TableColumn<Agent>[] = [
80
- {
81
- accessorKey: 'name',
82
- header: 'Name'
83
- },
84
- {
85
- accessorKey: 'role',
86
- header: 'Role'
87
- },
88
- {
89
- accessorKey: 'department',
90
- header: 'Department'
91
- },
92
- {
93
- accessorKey: 'tier',
94
- header: 'Tier'
95
- },
141
+ { accessorKey: 'name', header: 'Name' },
142
+ { accessorKey: 'role', header: 'Role' },
143
+ { accessorKey: 'department', header: 'Department' },
144
+ { accessorKey: 'tier', header: 'Tier' },
96
145
  {
97
146
  accessorFn: (row: Agent) => row.disc?.primary ?? '-',
98
147
  id: 'disc',
99
- header: 'DISC'
100
- },
101
- {
102
- accessorKey: 'mbti',
103
- header: 'MBTI'
148
+ header: 'DISC',
104
149
  },
105
- {
106
- id: 'actions',
107
- header: ''
108
- }
150
+ { accessorKey: 'mbti', header: 'MBTI' },
151
+ { id: 'activity', header: 'Activity (7d)' },
152
+ { id: 'actions', header: '' },
109
153
  ]
110
154
 
111
155
  function goToAgent(id: string) {
@@ -134,7 +178,7 @@ function goToAgent(id: string) {
134
178
  empty-title="No agents found"
135
179
  empty-icon="i-lucide-users"
136
180
  loading-label="Loading agents"
137
- :on-retry="() => refresh()"
181
+ :on-retry="() => refreshAll()"
138
182
  >
139
183
  <div class="flex flex-wrap items-center gap-3 mb-4">
140
184
  <UInput
@@ -195,8 +239,33 @@ function goToAgent(id: string) {
195
239
  <template #mbti-cell="{ row }">
196
240
  <span class="font-mono text-sm">{{ row.original.mbti || '-' }}</span>
197
241
  </template>
242
+ <template #activity-cell="{ row }">
243
+ <template v-if="deptActivity(row.original.department)">
244
+ <div class="flex items-center gap-2">
245
+ <span class="inline-block size-2 rounded-full bg-green-500" />
246
+ <span class="text-xs font-mono">
247
+ {{ deptActivity(row.original.department)?.call_count ?? 0 }} calls
248
+ </span>
249
+ </div>
250
+ </template>
251
+ <span v-else class="text-xs text-muted">—</span>
252
+ </template>
198
253
  <template #actions-cell="{ row }">
199
- <UButton size="xs" variant="ghost" icon="i-lucide-arrow-right" @click="goToAgent(row.original.id)" />
254
+ <UButton
255
+ :icon="copied === row.original.id ? 'i-lucide-check' : 'i-lucide-copy'"
256
+ :color="copied === row.original.id ? 'success' : 'neutral'"
257
+ variant="ghost"
258
+ size="xs"
259
+ aria-label="Copy agent mention"
260
+ @click.stop="copyAgentMention(row.original)"
261
+ />
262
+ <UButton
263
+ size="xs"
264
+ variant="ghost"
265
+ icon="i-lucide-arrow-right"
266
+ aria-label="Open agent detail"
267
+ @click="goToAgent(row.original.id)"
268
+ />
200
269
  </template>
201
270
  </UTable>
202
271
 
@@ -1,14 +1,153 @@
1
1
  <script setup lang="ts">
2
- import type { HealthCheck } from '~/types'
2
+ // PR70 v2.87.0 Health page polish.
3
+ // - 30s auto-refresh (paused while tab is hidden) + manual refresh
4
+ // - Last-checked timestamp in header
5
+ // - Severity-aware rendering (fail = red, warn = yellow)
6
+ // - Copy-fix button when a check has a fix command
7
+ // - Healthy banner ignores warnings (only blocking failures matter)
8
+
9
+ interface HealthCheck {
10
+ name: string
11
+ passed: boolean
12
+ fix: string
13
+ severity: 'fail' | 'warn'
14
+ }
15
+
16
+ interface HealthPayload {
17
+ checks: HealthCheck[]
18
+ passed: number
19
+ total: number
20
+ failed_blocking: number
21
+ warning_count: number
22
+ healthy: boolean
23
+ ts: string
24
+ }
3
25
 
4
26
  const { fetchApi } = useApi()
27
+ const toast = useToast()
28
+
29
+ const {
30
+ data,
31
+ status,
32
+ error,
33
+ refresh,
34
+ } = await fetchApi<HealthPayload>('/api/health')
35
+
36
+ // ─── Auto-refresh ───────────────────────────────────────────────────────
37
+
38
+ let pollTimer: ReturnType<typeof setInterval> | null = null
39
+
40
+ function startPolling() {
41
+ stopPolling()
42
+ pollTimer = setInterval(() => {
43
+ refresh()
44
+ }, 30_000)
45
+ }
46
+
47
+ function stopPolling() {
48
+ if (pollTimer !== null) {
49
+ clearInterval(pollTimer)
50
+ pollTimer = null
51
+ }
52
+ }
53
+
54
+ function handleVisibility() {
55
+ if (typeof document === 'undefined') return
56
+ if (document.hidden) {
57
+ stopPolling()
58
+ } else {
59
+ refresh()
60
+ startPolling()
61
+ }
62
+ }
63
+
64
+ onMounted(() => {
65
+ if (typeof document !== 'undefined') {
66
+ document.addEventListener('visibilitychange', handleVisibility)
67
+ }
68
+ startPolling()
69
+ })
70
+
71
+ onBeforeUnmount(() => {
72
+ stopPolling()
73
+ if (typeof document !== 'undefined') {
74
+ document.removeEventListener('visibilitychange', handleVisibility)
75
+ }
76
+ })
77
+
78
+ // ─── Copy fix ───────────────────────────────────────────────────────────
79
+
80
+ const copied = ref<string | null>(null)
81
+ let copyTimer: ReturnType<typeof setTimeout> | null = null
82
+
83
+ async function copyFix(check: HealthCheck) {
84
+ if (!check.fix) return
85
+ if (typeof navigator === 'undefined' || !navigator.clipboard) {
86
+ toast.add({ title: 'Clipboard unavailable', color: 'warning' })
87
+ return
88
+ }
89
+ try {
90
+ await navigator.clipboard.writeText(check.fix)
91
+ copied.value = check.name
92
+ if (copyTimer) clearTimeout(copyTimer)
93
+ copyTimer = setTimeout(() => { copied.value = null; copyTimer = null }, 1500)
94
+ toast.add({
95
+ title: 'Fix copied',
96
+ description: check.fix,
97
+ color: 'success',
98
+ })
99
+ } catch (err) {
100
+ toast.add({
101
+ title: 'Copy failed',
102
+ description: err instanceof Error ? err.message : 'unknown error',
103
+ color: 'error',
104
+ })
105
+ }
106
+ }
5
107
 
6
- const { data, status, error, refresh } = await fetchApi<{ checks: HealthCheck[], passed: number, total: number }>('/api/health')
108
+ onBeforeUnmount(() => {
109
+ if (copyTimer) clearTimeout(copyTimer)
110
+ })
7
111
 
8
- const checks = computed(() => data.value?.checks ?? [])
112
+ // ─── Format helpers ─────────────────────────────────────────────────────
113
+
114
+ function formatTs(iso: string | undefined): string {
115
+ if (!iso) return ''
116
+ try {
117
+ return new Intl.DateTimeFormat('en-US', {
118
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
119
+ }).format(new Date(iso))
120
+ } catch {
121
+ return iso
122
+ }
123
+ }
124
+
125
+ type CheckStatus = 'pass' | 'warn' | 'fail'
126
+
127
+ function statusOf(c: HealthCheck): CheckStatus {
128
+ if (c.passed) return 'pass'
129
+ return c.severity === 'warn' ? 'warn' : 'fail'
130
+ }
131
+
132
+ const STATUS_META: Record<CheckStatus, { icon: string; color: string; label: string }> = {
133
+ pass: { icon: 'i-lucide-check-circle', color: 'text-green-500', label: 'Pass' },
134
+ warn: { icon: 'i-lucide-alert-circle', color: 'text-yellow-500', label: 'Warn' },
135
+ fail: { icon: 'i-lucide-x-circle', color: 'text-red-500', label: 'Fail' },
136
+ }
137
+
138
+ function statusBadgeColor(s: CheckStatus): 'success' | 'warning' | 'error' {
139
+ return s === 'pass' ? 'success' : s === 'warn' ? 'warning' : 'error'
140
+ }
141
+
142
+ // ─── Aggregate display ──────────────────────────────────────────────────
143
+
144
+ const checks = computed<HealthCheck[]>(() => data.value?.checks ?? [])
9
145
  const passed = computed(() => data.value?.passed ?? 0)
10
146
  const total = computed(() => data.value?.total ?? 0)
11
- const allPassed = computed(() => passed.value === total.value && total.value > 0)
147
+ const failedBlocking = computed(() => data.value?.failed_blocking ?? 0)
148
+ const warningCount = computed(() => data.value?.warning_count ?? 0)
149
+ const allPassed = computed(() => failedBlocking.value === 0 && warningCount.value === 0 && total.value > 0)
150
+ const someWarnings = computed(() => failedBlocking.value === 0 && warningCount.value > 0)
12
151
  </script>
13
152
 
14
153
  <template>
@@ -19,11 +158,28 @@ const allPassed = computed(() => passed.value === total.value && total.value > 0
19
158
  <UDashboardSidebarCollapse />
20
159
  </template>
21
160
  <template #trailing>
161
+ <span
162
+ v-if="data?.ts"
163
+ class="text-xs text-muted"
164
+ :title="data.ts"
165
+ >
166
+ Last checked {{ formatTs(data.ts) }}
167
+ </span>
22
168
  <UBadge
23
169
  v-if="data"
24
170
  :label="`${passed}/${total}`"
25
- :color="allPassed ? 'success' : 'warning'"
171
+ :color="allPassed ? 'success' : someWarnings ? 'warning' : 'error'"
26
172
  variant="subtle"
173
+ class="ml-3"
174
+ />
175
+ </template>
176
+ <template #right>
177
+ <UButton
178
+ label="Refresh"
179
+ variant="ghost"
180
+ icon="i-lucide-refresh-cw"
181
+ size="sm"
182
+ @click="refresh()"
27
183
  />
28
184
  </template>
29
185
  </UDashboardNavbar>
@@ -39,45 +195,81 @@ const allPassed = computed(() => passed.value === total.value && total.value > 0
39
195
  loading-label="Loading health checks"
40
196
  :on-retry="() => refresh()"
41
197
  >
42
- <!-- Overall Status -->
198
+ <!-- Overall banner -->
43
199
  <div
44
200
  class="mb-6 rounded-lg border p-6 text-center"
45
- :class="allPassed ? 'border-green-500/30 bg-green-500/5' : 'border-yellow-500/30 bg-yellow-500/5'"
201
+ :class="allPassed
202
+ ? 'border-green-500/30 bg-green-500/5'
203
+ : someWarnings
204
+ ? 'border-yellow-500/30 bg-yellow-500/5'
205
+ : 'border-red-500/30 bg-red-500/5'"
46
206
  >
47
207
  <UIcon
48
- :name="allPassed ? 'i-lucide-check-circle' : 'i-lucide-alert-circle'"
49
- :class="allPassed ? 'text-green-500' : 'text-yellow-500'"
208
+ :name="allPassed
209
+ ? 'i-lucide-check-circle'
210
+ : someWarnings
211
+ ? 'i-lucide-alert-circle'
212
+ : 'i-lucide-x-circle'"
213
+ :class="allPassed
214
+ ? 'text-green-500'
215
+ : someWarnings ? 'text-yellow-500' : 'text-red-500'"
50
216
  class="size-12"
51
217
  />
52
218
  <p class="mt-2 text-lg font-semibold text-highlighted">
53
- {{ allPassed ? 'All Checks Passing' : `${total - passed} Check(s) Failing` }}
219
+ <template v-if="allPassed">All Checks Passing</template>
220
+ <template v-else-if="someWarnings">
221
+ {{ warningCount }} Warning{{ warningCount === 1 ? '' : 's' }}
222
+ </template>
223
+ <template v-else>
224
+ {{ failedBlocking }} Blocking Failure{{ failedBlocking === 1 ? '' : 's' }}
225
+ </template>
226
+ </p>
227
+ <p class="text-sm text-muted">
228
+ {{ passed }} of {{ total }} checks passed
229
+ <template v-if="warningCount && failedBlocking">
230
+ · {{ warningCount }} warn · {{ failedBlocking }} blocking
231
+ </template>
54
232
  </p>
55
- <p class="text-sm text-muted">{{ passed }} of {{ total }} checks passed</p>
56
233
  </div>
57
234
 
58
- <!-- Individual Checks -->
235
+ <!-- Check list -->
59
236
  <div class="space-y-3">
60
237
  <div
61
238
  v-for="check in checks"
62
239
  :key="check.name"
63
- class="flex items-start gap-3 rounded-lg border border-default p-4"
240
+ class="flex items-start gap-3 rounded-lg border p-4"
241
+ :class="{
242
+ 'border-default': check.passed,
243
+ 'border-yellow-500/30 bg-yellow-500/5': !check.passed && check.severity === 'warn',
244
+ 'border-red-500/30 bg-red-500/5': !check.passed && check.severity === 'fail',
245
+ }"
64
246
  >
65
247
  <UIcon
66
- :name="check.passed ? 'i-lucide-check-circle' : 'i-lucide-x-circle'"
67
- :class="check.passed ? 'text-green-500' : 'text-red-500'"
248
+ :name="STATUS_META[statusOf(check)].icon"
249
+ :class="STATUS_META[statusOf(check)].color"
68
250
  class="mt-0.5 size-5 shrink-0"
69
251
  />
70
- <div class="flex-1">
252
+ <div class="flex-1 min-w-0">
71
253
  <h4 class="font-medium text-highlighted">{{ check.name }}</h4>
72
254
  <p v-if="!check.passed && check.fix" class="mt-1 text-sm text-muted">
73
- Fix: {{ check.fix }}
255
+ Fix: <code class="font-mono text-xs">{{ check.fix }}</code>
74
256
  </p>
75
257
  </div>
258
+ <UButton
259
+ v-if="!check.passed && check.fix"
260
+ :icon="copied === check.name ? 'i-lucide-check' : 'i-lucide-copy'"
261
+ :color="copied === check.name ? 'success' : 'neutral'"
262
+ variant="ghost"
263
+ size="xs"
264
+ aria-label="Copy fix command"
265
+ @click="copyFix(check)"
266
+ />
76
267
  <UBadge
77
- :label="check.passed ? 'Pass' : 'Fail'"
78
- :color="check.passed ? 'success' : 'error'"
268
+ :label="STATUS_META[statusOf(check)].label"
269
+ :color="statusBadgeColor(statusOf(check))"
79
270
  variant="subtle"
80
271
  size="sm"
272
+ class="shrink-0"
81
273
  />
82
274
  </div>
83
275
  </div>
@@ -175,6 +175,9 @@ export interface HealthCheck {
175
175
  name: string
176
176
  passed: boolean
177
177
  fix: string
178
+ // PR70 v2.87.0 — backend now tags every check with a severity.
179
+ // 'fail' is must-pass; 'warn' is recommended but non-blocking.
180
+ severity?: 'fail' | 'warn'
178
181
  }
179
182
 
180
183
  export interface Persona {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "2.85.0",
3
+ "version": "2.87.0",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "2.85.0"
3
+ version = "2.87.0"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -173,6 +173,51 @@ def agents(dept: Optional[str] = Query(None)):
173
173
  return {"agents": data, "total": len(data)}
174
174
 
175
175
 
176
+ @app.get("/api/agents/activity")
177
+ def agents_activity(period: str = "week"):
178
+ """Per-department activity from the PR47 LLM cost telemetry.
179
+
180
+ Returns ``{by_department: {dev: {call_count, total_cost_usd,
181
+ total_tokens_in, total_tokens_out}}}`` derived from rows whose
182
+ ``category`` field starts with ``subagent:``. Each agent's
183
+ dispatch is currently tagged at the department level — finer
184
+ per-agent attribution will land when orchestrators set
185
+ ``ARKA_CALL_CATEGORY=subagent:<dept>:<agent>``.
186
+ """
187
+ try:
188
+ from core.runtime.llm_cost_telemetry import summarise, VALID_PERIODS
189
+ except Exception: # pragma: no cover - import guard
190
+ return {"by_department": {}, "period": period}
191
+ if period not in VALID_PERIODS:
192
+ period = "week"
193
+ summary = summarise(period=period)
194
+ out: dict[str, dict] = {}
195
+ for category, row in (summary.by_category or {}).items():
196
+ if not isinstance(category, str) or not category.startswith("subagent:"):
197
+ continue
198
+ dept = category.split(":", 1)[1] or "unknown"
199
+ bucket = out.setdefault(dept, {
200
+ "call_count": 0,
201
+ "total_cost_usd": 0.0,
202
+ "any_cost_known": False,
203
+ "total_tokens_in": 0,
204
+ "total_tokens_out": 0,
205
+ })
206
+ bucket["call_count"] += row.get("call_count", 0)
207
+ bucket["total_tokens_in"] += row.get("total_tokens_in", 0)
208
+ bucket["total_tokens_out"] += row.get("total_tokens_out", 0)
209
+ cost = row.get("total_cost_usd")
210
+ if isinstance(cost, (int, float)):
211
+ bucket["total_cost_usd"] += float(cost)
212
+ bucket["any_cost_known"] = True
213
+ for dept, b in out.items():
214
+ if not b.pop("any_cost_known"):
215
+ b["total_cost_usd"] = None
216
+ else:
217
+ b["total_cost_usd"] = round(b["total_cost_usd"], 6)
218
+ return {"by_department": out, "period": period}
219
+
220
+
176
221
  @app.get("/api/agents/{agent_id}")
177
222
  def agent_detail(agent_id: str):
178
223
  """Get full agent detail including YAML data."""
@@ -569,18 +614,38 @@ def knowledge_search(q: str = Query(...), top_k: int = Query(5)):
569
614
 
570
615
  @app.get("/api/health")
571
616
  def health():
572
- checks = []
617
+ """PR70 v2.87.0 — per-check severity + response timestamp.
618
+
619
+ Each check now carries a `severity` field:
620
+ - "fail" — must-pass; missing breaks ArkaOS
621
+ - "warn" — recommended; missing means a degraded but workable env
622
+
623
+ Response also carries `ts` so the UI can show "last checked".
624
+ Frontend polls every 30s and surfaces copy-fix buttons.
625
+ """
626
+ from datetime import datetime, timezone
627
+
628
+ checks: list[dict] = []
573
629
  arkaos_home = Path.home() / ".arkaos"
574
630
 
575
- def check(name, condition, fix=""):
576
- checks.append({"name": name, "passed": condition, "fix": fix})
631
+ def check(name: str, condition: bool, fix: str = "", severity: str = "fail"):
632
+ checks.append({
633
+ "name": name,
634
+ "passed": condition,
635
+ "fix": fix,
636
+ "severity": severity,
637
+ })
577
638
 
578
- check("install_dir", arkaos_home.exists(), "Run: npx arkaos install")
579
- check("manifest", (arkaos_home / "install-manifest.json").exists(), "Run: npx arkaos install")
639
+ check("install_dir", arkaos_home.exists(), "npx arkaos install")
640
+ check("manifest", (arkaos_home / "install-manifest.json").exists(),
641
+ "npx arkaos install")
580
642
  check("constitution", (ARKAOS_ROOT / "config" / "constitution.yaml").exists())
581
- check("agents_registry", (ARKAOS_ROOT / "knowledge" / "agents-registry-v2.json").exists())
582
- check("commands_registry", (ARKAOS_ROOT / "knowledge" / "commands-registry-v2.json").exists())
583
- check("hooks_dir", (arkaos_home / "config" / "hooks").exists(), "Run: npx arkaos install")
643
+ check("agents_registry",
644
+ (ARKAOS_ROOT / "knowledge" / "agents-registry-v2.json").exists())
645
+ check("commands_registry",
646
+ (ARKAOS_ROOT / "knowledge" / "commands-registry-v2.json").exists())
647
+ check("hooks_dir", (arkaos_home / "config" / "hooks").exists(),
648
+ "npx arkaos install")
584
649
 
585
650
  try:
586
651
  subprocess.run(["python3", "--version"], capture_output=True, timeout=2)
@@ -588,10 +653,34 @@ def health():
588
653
  except Exception:
589
654
  check("python", False, "Install Python 3.11+")
590
655
 
591
- check("knowledge_db", (arkaos_home / "knowledge.db").exists(), "Run: npx arkaos index")
656
+ # Telemetry + knowledge warn-only; missing them is a degraded
657
+ # but workable state (new installs, never-indexed-anything).
658
+ check("knowledge_db", (arkaos_home / "knowledge.db").exists(),
659
+ "Open the Knowledge tab and ingest a source",
660
+ severity="warn")
661
+ check("profile",
662
+ (arkaos_home / "profile.json").exists(),
663
+ "Open Settings → Profile to introduce yourself",
664
+ severity="warn")
592
665
 
593
666
  passed = sum(1 for c in checks if c["passed"])
594
- return {"checks": checks, "passed": passed, "total": len(checks), "healthy": passed == len(checks)}
667
+ failed_blocking = sum(
668
+ 1 for c in checks
669
+ if not c["passed"] and c["severity"] == "fail"
670
+ )
671
+ warning_count = sum(
672
+ 1 for c in checks
673
+ if not c["passed"] and c["severity"] == "warn"
674
+ )
675
+ return {
676
+ "checks": checks,
677
+ "passed": passed,
678
+ "total": len(checks),
679
+ "failed_blocking": failed_blocking,
680
+ "warning_count": warning_count,
681
+ "healthy": failed_blocking == 0,
682
+ "ts": datetime.now(timezone.utc).isoformat(),
683
+ }
595
684
 
596
685
 
597
686
  # --- Personas ---