arkaos 3.33.0 → 3.35.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
- 3.33.0
1
+ 3.35.0
@@ -0,0 +1,62 @@
1
+ <script setup lang="ts">
2
+ // PR91a v3.35.0 — "What's missing?" home page card.
3
+ //
4
+ // Lists empty departments (high severity) and depts missing a Tier 2
5
+ // specialist (medium). Each suggestion is a link to /agents/new.
6
+
7
+ interface Suggestion {
8
+ department: string
9
+ reason: string
10
+ recommended_tier: 1 | 2
11
+ severity: 'high' | 'medium'
12
+ }
13
+
14
+ const { fetchApi } = useApi()
15
+ const { data, status } = fetchApi<{ suggestions: Suggestion[], total_gaps: number }>(
16
+ '/api/agents/suggestions?limit=6',
17
+ )
18
+
19
+ const suggestions = computed<Suggestion[]>(() => data.value?.suggestions ?? [])
20
+
21
+ function severityColor(s: string): 'error' | 'warning' | 'neutral' {
22
+ return s === 'high' ? 'error' : s === 'medium' ? 'warning' : 'neutral'
23
+ }
24
+ </script>
25
+
26
+ <template>
27
+ <UCard v-if="status !== 'pending' && suggestions.length > 0">
28
+ <template #header>
29
+ <div class="flex items-center justify-between">
30
+ <div>
31
+ <h3 class="text-lg font-semibold">What's missing?</h3>
32
+ <p class="text-xs text-muted mt-0.5">
33
+ {{ data?.total_gaps }} gap{{ data?.total_gaps === 1 ? '' : 's' }} across departments. Showing top {{ suggestions.length }}.
34
+ </p>
35
+ </div>
36
+ <UIcon name="i-lucide-sparkles" class="size-5 text-primary" />
37
+ </div>
38
+ </template>
39
+
40
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-2">
41
+ <NuxtLink
42
+ v-for="s in suggestions"
43
+ :key="`${s.department}:${s.recommended_tier}`"
44
+ to="/agents/new"
45
+ class="flex items-center gap-3 rounded-lg border border-default p-3 hover:border-primary/40 transition-colors"
46
+ >
47
+ <UBadge
48
+ :label="s.severity"
49
+ :color="severityColor(s.severity)"
50
+ variant="subtle"
51
+ size="xs"
52
+ />
53
+ <div class="flex-1 min-w-0">
54
+ <p class="text-sm font-semibold capitalize truncate">{{ s.department }}</p>
55
+ <p class="text-xs text-muted truncate">{{ s.reason }}</p>
56
+ </div>
57
+ <span class="text-xs font-mono text-muted shrink-0">T{{ s.recommended_tier }}</span>
58
+ <UIcon name="i-lucide-arrow-right" class="size-4 text-muted shrink-0" />
59
+ </NuxtLink>
60
+ </div>
61
+ </UCard>
62
+ </template>
@@ -83,6 +83,13 @@ const links = [[{
83
83
  onSelect: () => {
84
84
  open.value = false
85
85
  }
86
+ }, {
87
+ label: 'Audit',
88
+ icon: 'i-lucide-shield-alert',
89
+ to: '/audit',
90
+ onSelect: () => {
91
+ open.value = false
92
+ }
86
93
  }, {
87
94
  label: 'Trash',
88
95
  icon: 'i-lucide-trash-2',
@@ -0,0 +1,125 @@
1
+ <script setup lang="ts">
2
+ // PR90d v3.34.0 — Audit log page.
3
+ //
4
+ // Lists hook bypass / block events from the enforcement telemetry log.
5
+ // Operators filter by kind (bypass / blocked) and tool name.
6
+
7
+ interface AuditEvent {
8
+ ts: string
9
+ tool: string
10
+ reason: string
11
+ cwd: string
12
+ bypass_used: boolean
13
+ kind: 'bypass' | 'blocked'
14
+ }
15
+
16
+ const { fetchApi } = useApi()
17
+
18
+ const kind = ref<'all' | 'bypass' | 'blocked'>('all')
19
+ const toolFilter = ref('')
20
+ const limit = ref(100)
21
+
22
+ const { data, status, error, refresh } = await fetchApi<{
23
+ events: AuditEvent[]
24
+ total: number
25
+ }>(
26
+ '/api/audit',
27
+ {
28
+ query: computed(() => ({
29
+ limit: limit.value,
30
+ kind: kind.value === 'all' ? undefined : kind.value,
31
+ tool: toolFilter.value.trim() || undefined,
32
+ })),
33
+ },
34
+ )
35
+
36
+ const events = computed<AuditEvent[]>(() => data.value?.events ?? [])
37
+
38
+ const kindOptions = [
39
+ { label: 'All kinds', value: 'all' },
40
+ { label: 'Bypass used', value: 'bypass' },
41
+ { label: 'Blocked', value: 'blocked' },
42
+ ]
43
+
44
+ function formatTs(ts: string): string {
45
+ if (!ts) return '—'
46
+ try {
47
+ return new Date(ts).toLocaleString()
48
+ } catch {
49
+ return ts
50
+ }
51
+ }
52
+
53
+ function kindColor(k: string): 'warning' | 'error' | 'neutral' {
54
+ return k === 'bypass' ? 'warning' : k === 'blocked' ? 'error' : 'neutral'
55
+ }
56
+
57
+ watch([kind, toolFilter, limit], async () => {
58
+ await refresh()
59
+ })
60
+ </script>
61
+
62
+ <template>
63
+ <UDashboardPanel id="audit">
64
+ <template #header>
65
+ <UDashboardNavbar title="Audit log">
66
+ <template #leading>
67
+ <UDashboardSidebarCollapse />
68
+ </template>
69
+ <template #trailing>
70
+ <UBadge v-if="data?.total" :label="`${data.total} event${data.total === 1 ? '' : 's'}`" variant="subtle" />
71
+ </template>
72
+ </UDashboardNavbar>
73
+ </template>
74
+
75
+ <template #body>
76
+ <DashboardState
77
+ :status="status"
78
+ :error="error"
79
+ :empty="!events.length"
80
+ empty-title="No audit events"
81
+ empty-description="Hook bypass + block events show up here. Nothing yet — good news."
82
+ empty-icon="i-lucide-shield-check"
83
+ loading-label="Loading audit"
84
+ :on-retry="() => refresh()"
85
+ >
86
+ <div class="flex flex-wrap items-center gap-3 mb-4">
87
+ <USelect
88
+ v-model="kind"
89
+ :items="kindOptions"
90
+ placeholder="Kind"
91
+ class="min-w-40"
92
+ aria-label="Filter by kind"
93
+ />
94
+ <UInput
95
+ v-model="toolFilter"
96
+ class="max-w-xs"
97
+ icon="i-lucide-wrench"
98
+ placeholder="Filter by tool…"
99
+ />
100
+ <span class="ml-auto text-xs text-muted">
101
+ {{ events.length }} match{{ events.length === 1 ? '' : 'es' }}
102
+ </span>
103
+ </div>
104
+
105
+ <ul class="space-y-2 max-w-5xl">
106
+ <li
107
+ v-for="(ev, idx) in events"
108
+ :key="`${ev.ts}-${idx}`"
109
+ class="rounded-lg border border-default p-3"
110
+ >
111
+ <div class="flex items-center gap-3 flex-wrap text-xs mb-1.5">
112
+ <UBadge :label="ev.kind" :color="kindColor(ev.kind)" variant="subtle" size="sm" />
113
+ <UBadge :label="ev.tool || '—'" variant="outline" size="sm" />
114
+ <span class="text-muted font-mono">{{ formatTs(ev.ts) }}</span>
115
+ </div>
116
+ <p class="text-sm">{{ ev.reason || '(no reason recorded)' }}</p>
117
+ <p v-if="ev.cwd" class="text-xs text-muted font-mono mt-1 truncate" :title="ev.cwd">
118
+ {{ ev.cwd }}
119
+ </p>
120
+ </li>
121
+ </ul>
122
+ </DashboardState>
123
+ </template>
124
+ </UDashboardPanel>
125
+ </template>
@@ -204,6 +204,9 @@ function copyCommand(cmd: string) {
204
204
  </div>
205
205
  </UCard>
206
206
 
207
+ <!-- PR91a v3.35.0 — Agent gap suggestions -->
208
+ <AgentSuggestionsCard class="mb-6" />
209
+
207
210
  <!-- PR84d v3.10.0 — Top departments + Recent personas row -->
208
211
  <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
209
212
  <div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.33.0",
3
+ "version": "3.35.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 = "3.33.0"
3
+ version = "3.35.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"}
@@ -1567,6 +1567,63 @@ def agent_export_to_vault(agent_id: str):
1567
1567
  return {"exported": True, "path": str(res.path), "vault_path": str(res.vault_path)}
1568
1568
 
1569
1569
 
1570
+ # --- Agent gap suggestions (PR91a v3.35.0) ---
1571
+
1572
+ _KNOWN_DEPARTMENTS = (
1573
+ "dev", "marketing", "brand", "finance", "strategy", "ecom", "kb",
1574
+ "ops", "pm", "saas", "landing", "content", "community", "sales",
1575
+ "leadership", "org",
1576
+ )
1577
+
1578
+
1579
+ @app.get("/api/agents/suggestions")
1580
+ def agents_suggestions(limit: int = 6):
1581
+ """PR91a v3.35.0 — recommend agents the operator should consider creating.
1582
+
1583
+ Heuristics:
1584
+ 1. Departments with zero agents at all (severity: high).
1585
+ 2. Departments missing a Tier 2 specialist (severity: medium).
1586
+
1587
+ Each suggestion includes ``reason`` and ``recommended_tier``. Used
1588
+ by the home page "What's missing?" card.
1589
+ """
1590
+ agents = _load_agents()
1591
+ by_dept: dict[str, dict] = {}
1592
+ for a in agents:
1593
+ dept = a.get("department") or ""
1594
+ if not dept:
1595
+ continue
1596
+ row = by_dept.setdefault(dept, {"count": 0, "tiers": set()})
1597
+ row["count"] += 1
1598
+ try:
1599
+ row["tiers"].add(int(a.get("tier") or 99))
1600
+ except (TypeError, ValueError):
1601
+ pass
1602
+
1603
+ suggestions: list[dict] = []
1604
+ for dept in _KNOWN_DEPARTMENTS:
1605
+ info = by_dept.get(dept)
1606
+ if info is None or info["count"] == 0:
1607
+ suggestions.append({
1608
+ "department": dept,
1609
+ "reason": "no agents — department is empty",
1610
+ "recommended_tier": 1,
1611
+ "severity": "high",
1612
+ })
1613
+ continue
1614
+ if 2 not in info["tiers"]:
1615
+ suggestions.append({
1616
+ "department": dept,
1617
+ "reason": "no Tier 2 specialist",
1618
+ "recommended_tier": 2,
1619
+ "severity": "medium",
1620
+ })
1621
+ return {
1622
+ "suggestions": suggestions[: max(0, int(limit))],
1623
+ "total_gaps": len(suggestions),
1624
+ }
1625
+
1626
+
1570
1627
  # --- Departments (PR89a v3.27.0) ---
1571
1628
 
1572
1629
  @app.get("/api/departments")
@@ -2251,6 +2308,58 @@ def _last_commit_days(project_path: str) -> Optional[int]:
2251
2308
  return None
2252
2309
 
2253
2310
 
2311
+ @app.get("/api/audit")
2312
+ def audit_log(
2313
+ limit: int = 100,
2314
+ kind: Optional[str] = None,
2315
+ tool: Optional[str] = None,
2316
+ ):
2317
+ """PR90d v3.34.0 — paginated audit log of bypass/block events.
2318
+
2319
+ Reads the same telemetry file as `_recent_incidents` but returns
2320
+ a larger window and accepts filter params. ``kind`` is one of
2321
+ ``"bypass"`` / ``"blocked"``. ``tool`` filters by tool name
2322
+ (Edit/Write/Bash/…).
2323
+ """
2324
+ log = Path.home() / ".arkaos" / "telemetry" / "enforcement.jsonl"
2325
+ if not log.exists():
2326
+ return {"events": [], "total": 0}
2327
+ try:
2328
+ text = log.read_text(encoding="utf-8", errors="replace")
2329
+ except OSError:
2330
+ return {"events": [], "total": 0}
2331
+ events: list[dict] = []
2332
+ cap = max(0, min(int(limit), 500))
2333
+ for line in reversed(text.splitlines()):
2334
+ if len(events) >= cap:
2335
+ break
2336
+ if not line.strip():
2337
+ continue
2338
+ try:
2339
+ entry = json.loads(line)
2340
+ except json.JSONDecodeError:
2341
+ continue
2342
+ bypass = bool(entry.get("bypass_used"))
2343
+ allowed = entry.get("allow")
2344
+ if not bypass and allowed is not False:
2345
+ continue
2346
+ row_kind = "bypass" if bypass else "blocked"
2347
+ if kind and kind != row_kind:
2348
+ continue
2349
+ row_tool = entry.get("tool", "")
2350
+ if tool and tool != row_tool:
2351
+ continue
2352
+ events.append({
2353
+ "ts": entry.get("ts", ""),
2354
+ "tool": row_tool,
2355
+ "reason": entry.get("reason", ""),
2356
+ "cwd": entry.get("cwd", ""),
2357
+ "bypass_used": bypass,
2358
+ "kind": row_kind,
2359
+ })
2360
+ return {"events": events, "total": len(events)}
2361
+
2362
+
2254
2363
  def _recent_incidents(limit: int = 8) -> list[dict]:
2255
2364
  """Recent enforcement / bypass events from telemetry.
2256
2365