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 +1 -1
- package/dashboard/app/components/AgentSuggestionsCard.vue +62 -0
- package/dashboard/app/layouts/default.vue +7 -0
- package/dashboard/app/pages/audit.vue +125 -0
- package/dashboard/app/pages/index.vue +3 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/dashboard-api.py +109 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
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>
|
|
@@ -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
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -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
|
|