arkaos 4.20.0 → 4.22.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 (80) hide show
  1. package/README.md +1 -1
  2. package/VERSION +1 -1
  3. package/arka/SKILL.md +2 -1
  4. package/config/claude-agents/marta-cqo.md +9 -0
  5. package/config/standards/{claude-md-overlays → stack-rules}/laravel.md +11 -0
  6. package/config/standards/{claude-md-overlays → stack-rules}/node.md +10 -1
  7. package/config/standards/{claude-md-overlays → stack-rules}/nuxt.md +12 -1
  8. package/config/standards/stack-rules/php.md +13 -0
  9. package/config/standards/{claude-md-overlays → stack-rules}/python.md +5 -0
  10. package/config/standards/stack-rules/react.md +14 -0
  11. package/config/standards/stack-rules/vue.md +13 -0
  12. package/core/cognition/evolve.py +299 -0
  13. package/core/cognition/evolve_cli.py +55 -0
  14. package/core/governance/stop_lint.py +266 -0
  15. package/core/governance/stop_lint_telemetry.py +132 -0
  16. package/core/governance/tool_loop_check.py +111 -0
  17. package/core/hooks/stop.py +73 -22
  18. package/core/runtime/pricing.py +53 -19
  19. package/core/synapse/layers.py +49 -476
  20. package/core/synapse/layers_base.py +99 -0
  21. package/core/synapse/layers_kb.py +395 -0
  22. package/core/sync/content_syncer.py +79 -10
  23. package/dashboard/app/components/AgentDependencyGraph.vue +5 -5
  24. package/dashboard/app/components/AgentEditDrawer.vue +120 -52
  25. package/dashboard/app/components/AgentSuggestionsCard.vue +4 -2
  26. package/dashboard/app/components/ConfirmDialog.vue +2 -2
  27. package/dashboard/app/components/DashboardState.vue +5 -3
  28. package/dashboard/app/components/GlobalSearch.vue +11 -7
  29. package/dashboard/app/components/KeyboardShortcutsHelp.vue +8 -6
  30. package/dashboard/app/components/KnowledgeSourcesList.vue +13 -9
  31. package/dashboard/app/components/MarkdownEditor.vue +1 -1
  32. package/dashboard/app/components/NotificationsBell.vue +8 -4
  33. package/dashboard/app/components/OnboardingTour.vue +24 -11
  34. package/dashboard/app/components/PersonaCloneDialog.vue +15 -8
  35. package/dashboard/app/components/PersonaWizard.vue +105 -72
  36. package/dashboard/app/components/SidebarFavoritesWidget.vue +5 -5
  37. package/dashboard/app/components/SidebarStatsWidget.vue +3 -1
  38. package/dashboard/app/components/Terminal.vue +9 -8
  39. package/dashboard/app/components/TextDiff.vue +27 -15
  40. package/dashboard/app/composables/useActivityFeed.ts +3 -3
  41. package/dashboard/app/composables/useApi.ts +4 -2
  42. package/dashboard/app/composables/useConfirmDialog.ts +1 -1
  43. package/dashboard/app/composables/useFavorites.ts +8 -8
  44. package/dashboard/app/composables/useTerminalThemes.ts +13 -13
  45. package/dashboard/app/composables/useThemeColor.ts +7 -4
  46. package/dashboard/app/pages/agents/[id].vue +319 -159
  47. package/dashboard/app/pages/agents/compare.vue +152 -52
  48. package/dashboard/app/pages/agents/index.vue +68 -55
  49. package/dashboard/app/pages/agents/new.vue +125 -58
  50. package/dashboard/app/pages/audit.vue +13 -6
  51. package/dashboard/app/pages/budget.vue +20 -18
  52. package/dashboard/app/pages/commands.vue +26 -22
  53. package/dashboard/app/pages/departments/[dept].vue +67 -31
  54. package/dashboard/app/pages/departments/compare.vue +85 -27
  55. package/dashboard/app/pages/departments/index.vue +31 -7
  56. package/dashboard/app/pages/health.vue +19 -12
  57. package/dashboard/app/pages/knowledge/index.vue +190 -78
  58. package/dashboard/app/pages/models.vue +74 -28
  59. package/dashboard/app/pages/personas/[id].vue +322 -219
  60. package/dashboard/app/pages/personas/archetypes.vue +20 -6
  61. package/dashboard/app/pages/personas/compare-with-agent.vue +109 -39
  62. package/dashboard/app/pages/personas/compare.vue +196 -124
  63. package/dashboard/app/pages/personas/index.vue +67 -60
  64. package/dashboard/app/pages/personas/new.vue +1 -1
  65. package/dashboard/app/pages/settings.vue +1 -0
  66. package/dashboard/app/pages/tasks.vue +80 -38
  67. package/dashboard/app/pages/trash.vue +11 -9
  68. package/dashboard/app/pages/workflows.vue +29 -19
  69. package/dashboard/nuxt.config.ts +2 -1
  70. package/dashboard/package.json +5 -0
  71. package/departments/kb/skills/doc-extraction/SKILL.md +99 -0
  72. package/departments/kb/skills/doc-redaction/SKILL.md +103 -0
  73. package/departments/kb/skills/knowledge-ops/SKILL.md +88 -0
  74. package/departments/ops/skills/github-ops/SKILL.md +89 -0
  75. package/departments/ops/skills/terminal-ops/SKILL.md +83 -0
  76. package/installer/index.js +1 -1
  77. package/knowledge/commands-registry.json +16 -3
  78. package/knowledge/skills-manifest.json +66 -1
  79. package/package.json +1 -1
  80. package/pyproject.toml +1 -1
@@ -21,7 +21,7 @@ function kindIcon(kind: string): string {
21
21
  success: 'i-lucide-check-circle',
22
22
  warning: 'i-lucide-alert-triangle',
23
23
  error: 'i-lucide-x-circle',
24
- info: 'i-lucide-info',
24
+ info: 'i-lucide-info'
25
25
  }[kind] ?? 'i-lucide-circle'
26
26
  }
27
27
 
@@ -30,7 +30,7 @@ function kindColor(kind: string): string {
30
30
  success: 'text-emerald-500',
31
31
  warning: 'text-amber-500',
32
32
  error: 'text-rose-500',
33
- info: 'text-blue-500',
33
+ info: 'text-blue-500'
34
34
  }[kind] ?? 'text-muted'
35
35
  }
36
36
  </script>
@@ -56,7 +56,9 @@ function kindColor(kind: string): string {
56
56
  <template #content>
57
57
  <div class="p-2 border-b border-default flex items-center justify-between gap-2">
58
58
  <div>
59
- <p class="text-sm font-semibold">Recent activity</p>
59
+ <p class="text-sm font-semibold">
60
+ Recent activity
61
+ </p>
60
62
  <p class="text-xs text-muted">
61
63
  {{ feed.unreadCount.value }} unread ·
62
64
  {{ feed.events.value.length }} total
@@ -118,7 +120,9 @@ function kindColor(kind: string): string {
118
120
  <p v-if="ev.description" class="text-xs text-muted truncate">
119
121
  {{ ev.description }}
120
122
  </p>
121
- <p class="text-xs text-muted/70 mt-0.5">{{ formatRelative(ev.ts) }}</p>
123
+ <p class="text-xs text-muted/70 mt-0.5">
124
+ {{ formatRelative(ev.ts) }}
125
+ </p>
122
126
  </div>
123
127
  <UButton
124
128
  icon="i-lucide-x"
@@ -23,37 +23,37 @@ const steps: TourStep[] = [
23
23
  {
24
24
  icon: 'i-lucide-sparkles',
25
25
  title: 'Welcome to ArkaOS',
26
- body: 'A 4-minute tour of where to start. Press Esc anytime to skip — you can replay this from Settings.',
26
+ body: 'A 4-minute tour of where to start. Press Esc anytime to skip — you can replay this from Settings.'
27
27
  },
28
28
  {
29
29
  icon: 'i-lucide-users',
30
30
  title: 'Agents',
31
31
  body: 'Your specialist team. Browse the table, click a row for full detail (DNA, expertise, history). Create new ones with the AI draft on /agents/new.',
32
- cta: { label: 'Open /agents', to: '/agents' },
32
+ cta: { label: 'Open /agents', to: '/agents' }
33
33
  },
34
34
  {
35
35
  icon: 'i-lucide-user-plus',
36
36
  title: 'Personas',
37
37
  body: 'Behavioural profiles of real people (or archetypes) that seed your agents. Import from .md files / URLs, export as ZIP, clone to agents.',
38
- cta: { label: 'Open /personas', to: '/personas' },
38
+ cta: { label: 'Open /personas', to: '/personas' }
39
39
  },
40
40
  {
41
41
  icon: 'i-lucide-workflow',
42
42
  title: 'Workflows',
43
43
  body: 'YAML-defined orchestrations under departments/. Click a workflow for its phase flow, raw YAML, and recent runs.',
44
- cta: { label: 'Open /workflows', to: '/workflows' },
44
+ cta: { label: 'Open /workflows', to: '/workflows' }
45
45
  },
46
46
  {
47
47
  icon: 'i-lucide-wallet',
48
48
  title: 'Budget',
49
49
  body: 'Real LLM spend by provider/model/category, daily trend chart (7/14/30d), CSV export. Powered by PR47 telemetry.',
50
- cta: { label: 'Open /budget', to: '/budget' },
50
+ cta: { label: 'Open /budget', to: '/budget' }
51
51
  },
52
52
  {
53
53
  icon: 'i-lucide-keyboard',
54
54
  title: 'Power shortcuts',
55
- body: 'Press ? anywhere for the full keymap. Try / for search, g a for agents, g p for personas.',
56
- },
55
+ body: 'Press ? anywhere for the full keymap. Try / for search, g a for agents, g p for personas.'
56
+ }
57
57
  ]
58
58
 
59
59
  onMounted(() => {
@@ -100,24 +100,37 @@ const progress = computed(() => Math.round(((step.value + 1) / steps.length) * 1
100
100
  title="Welcome"
101
101
  >
102
102
  <template #content>
103
- <UCard>
103
+ <!-- step is clamped to [0, steps.length-1] by next()/back(), so
104
+ current is always defined — the v-if only narrows for the type
105
+ checker and never hides the card at runtime. -->
106
+ <UCard v-if="current">
104
107
  <template #header>
105
108
  <div class="flex items-center justify-between gap-3">
106
109
  <div class="flex items-center gap-3">
107
110
  <UIcon :name="current.icon" class="size-6 text-primary" />
108
111
  <div>
109
- <h2 class="text-lg font-bold">{{ current.title }}</h2>
112
+ <h2 class="text-lg font-bold">
113
+ {{ current.title }}
114
+ </h2>
110
115
  <p class="text-xs text-muted mt-0.5">
111
116
  Step {{ step + 1 }} of {{ steps.length }}
112
117
  </p>
113
118
  </div>
114
119
  </div>
115
- <UButton icon="i-lucide-x" variant="ghost" size="sm" aria-label="Skip" @click="dismiss" />
120
+ <UButton
121
+ icon="i-lucide-x"
122
+ variant="ghost"
123
+ size="sm"
124
+ aria-label="Skip"
125
+ @click="dismiss"
126
+ />
116
127
  </div>
117
128
  </template>
118
129
 
119
130
  <div class="space-y-4">
120
- <p class="text-sm">{{ current.body }}</p>
131
+ <p class="text-sm">
132
+ {{ current.body }}
133
+ </p>
121
134
  <UButton
122
135
  v-if="current.cta"
123
136
  :label="current.cta.label"
@@ -25,13 +25,13 @@ const cloning = ref(false)
25
25
 
26
26
  const departmentOptions = [
27
27
  'dev', 'marketing', 'brand', 'finance', 'strategy', 'ecom', 'kb', 'ops',
28
- 'pm', 'saas', 'landing', 'content', 'community', 'sales', 'leadership', 'org',
29
- ].map((d) => ({ label: d, value: d }))
28
+ 'pm', 'saas', 'landing', 'content', 'community', 'sales', 'leadership', 'org'
29
+ ].map(d => ({ label: d, value: d }))
30
30
 
31
31
  const tierOptions = [
32
32
  { label: 'Tier 1 — Squad Lead', value: 1 },
33
33
  { label: 'Tier 2 — Specialist', value: 2 },
34
- { label: 'Tier 3 — Support', value: 3 },
34
+ { label: 'Tier 3 — Support', value: 3 }
35
35
  ]
36
36
 
37
37
  async function clone() {
@@ -44,14 +44,14 @@ async function clone() {
44
44
  error?: string
45
45
  }>(`${apiBase}/api/personas/${props.personaId}/clone`, {
46
46
  method: 'POST',
47
- body: { department: department.value, tier: tier.value },
47
+ body: { department: department.value, tier: tier.value }
48
48
  })
49
49
  if (res.error || !res.agent_id) throw new Error(res.error ?? 'unknown error')
50
50
  toast.add({
51
51
  title: 'Cloned to agent',
52
52
  description: `${props.personaName} → ${res.agent_id} (${res.department})`,
53
53
  color: 'success',
54
- icon: 'i-lucide-copy-plus',
54
+ icon: 'i-lucide-copy-plus'
55
55
  })
56
56
  emit('cloned', res.agent_id)
57
57
  emit('update:modelValue', false)
@@ -59,7 +59,7 @@ async function clone() {
59
59
  toast.add({
60
60
  title: 'Clone failed',
61
61
  description: err instanceof Error ? err.message : 'unknown error',
62
- color: 'error',
62
+ color: 'error'
63
63
  })
64
64
  } finally {
65
65
  cloning.value = false
@@ -83,7 +83,9 @@ function close() {
83
83
  <template #header>
84
84
  <div class="flex items-center justify-between gap-3">
85
85
  <div>
86
- <h2 class="text-lg font-bold">Clone to Agent</h2>
86
+ <h2 class="text-lg font-bold">
87
+ Clone to Agent
88
+ </h2>
87
89
  <p class="text-sm text-muted mt-0.5">
88
90
  Create a new agent based on <strong>{{ personaName }}</strong>.
89
91
  </p>
@@ -116,7 +118,12 @@ function close() {
116
118
 
117
119
  <template #footer>
118
120
  <div class="flex items-center justify-end gap-2">
119
- <UButton label="Cancel" variant="ghost" :disabled="cloning" @click="close" />
121
+ <UButton
122
+ label="Cancel"
123
+ variant="ghost"
124
+ :disabled="cloning"
125
+ @click="close"
126
+ />
120
127
  <UButton
121
128
  label="Clone"
122
129
  icon="i-lucide-copy-plus"
@@ -32,9 +32,9 @@ const skipIngest = ref(false)
32
32
  const sourceLineCount = computed(() =>
33
33
  sources.value
34
34
  .split('\n')
35
- .map((s) => s.trim())
36
- .filter((s) => s.length > 0)
37
- .length,
35
+ .map(s => s.trim())
36
+ .filter(s => s.length > 0)
37
+ .length
38
38
  )
39
39
 
40
40
  // PR83a v3.3.0 — Mode 3: build from a free-text description (no chunks).
@@ -52,7 +52,7 @@ interface Archetype {
52
52
  description: string
53
53
  }
54
54
  const { data: archetypeData } = fetchApi<{ archetypes: Archetype[] }>(
55
- '/api/personas/archetypes',
55
+ '/api/personas/archetypes'
56
56
  )
57
57
  const archetypes = computed<Archetype[]>(() => archetypeData.value?.archetypes ?? [])
58
58
 
@@ -69,7 +69,7 @@ const route = useRoute()
69
69
  watch(archetypes, (list) => {
70
70
  const slug = String(route.query.archetype ?? '')
71
71
  if (!slug || list.length === 0) return
72
- const match = list.find((a) => a.id === slug)
72
+ const match = list.find(a => a.id === slug)
73
73
  if (match) applyArchetype(match)
74
74
  }, { immediate: true })
75
75
 
@@ -84,10 +84,10 @@ const ingestJobs = ref<Array<{
84
84
  let ws: WebSocket | null = null
85
85
  const allIngestComplete = computed(() =>
86
86
  ingestJobs.value.length > 0
87
- && ingestJobs.value.every((j) => j.status === 'completed' || j.status === 'failed'),
87
+ && ingestJobs.value.every(j => j.status === 'completed' || j.status === 'failed')
88
88
  )
89
89
  const ingestCompletedCount = computed(() =>
90
- ingestJobs.value.filter((j) => j.status === 'completed').length,
90
+ ingestJobs.value.filter(j => j.status === 'completed').length
91
91
  )
92
92
 
93
93
  // ─── Step 3 state ────────────────────────────────────────────────────────
@@ -104,18 +104,17 @@ const cloneTier = ref<'1' | '2' | '3'>('2')
104
104
 
105
105
  const departmentOptions = [
106
106
  'dev', 'marketing', 'brand', 'finance', 'strategy', 'ecom', 'kb', 'ops',
107
- 'pm', 'saas', 'landing', 'content', 'community', 'sales', 'leadership', 'org',
108
- ].map((d) => ({ label: d, value: d }))
107
+ 'pm', 'saas', 'landing', 'content', 'community', 'sales', 'leadership', 'org'
108
+ ].map(d => ({ label: d, value: d }))
109
109
 
110
110
  const tierOptions = [
111
111
  { label: 'Tier 1 — Squad Lead', value: '1' },
112
112
  { label: 'Tier 2 — Specialist', value: '2' },
113
- { label: 'Tier 3 — Support', value: '3' },
113
+ { label: 'Tier 3 — Support', value: '3' }
114
114
  ]
115
115
 
116
116
  // ─── Step 1 → 2 transition ───────────────────────────────────────────────
117
117
 
118
-
119
118
  async function startIngest() {
120
119
  if (mode.value === 'description') {
121
120
  // PR83a — no ingest, no chunks. Build directly from description.
@@ -132,22 +131,22 @@ async function startIngest() {
132
131
  }
133
132
  const cleaned = sources.value
134
133
  .split('\n')
135
- .map((s) => s.trim())
136
- .filter((s) => s.length > 0)
134
+ .map(s => s.trim())
135
+ .filter(s => s.length > 0)
137
136
  if (cleaned.length === 0 || !name.value.trim()) return
138
137
  step.value = 2
139
- ingestJobs.value = cleaned.map((source) => ({
138
+ ingestJobs.value = cleaned.map(source => ({
140
139
  source,
141
140
  status: 'queued',
142
- progress: 0,
141
+ progress: 0
143
142
  }))
144
143
  try {
145
144
  const res = await $fetch<{ jobs: Array<{ source: string, job_id?: string, error?: string }>, count: number }>(
146
145
  `${apiBase}/api/knowledge/ingest-bulk`,
147
- { method: 'POST', body: { sources: cleaned } },
146
+ { method: 'POST', body: { sources: cleaned } }
148
147
  )
149
148
  res.jobs.forEach((j) => {
150
- const row = ingestJobs.value.find((r) => r.source === j.source)
149
+ const row = ingestJobs.value.find(r => r.source === j.source)
151
150
  if (!row) return
152
151
  if (j.error) {
153
152
  row.status = 'failed'
@@ -162,13 +161,12 @@ async function startIngest() {
162
161
  toast.add({
163
162
  title: 'Ingest failed',
164
163
  description: err instanceof Error ? err.message : 'unknown error',
165
- color: 'error',
164
+ color: 'error'
166
165
  })
167
166
  step.value = 1
168
167
  }
169
168
  }
170
169
 
171
-
172
170
  function connectWebSocket() {
173
171
  if (ws && ws.readyState === WebSocket.OPEN) return
174
172
  const wsUrl = apiBase.replace('http://', 'ws://').replace('https://', 'wss://') + '/ws/tasks'
@@ -176,7 +174,7 @@ function connectWebSocket() {
176
174
  ws.onmessage = (event) => {
177
175
  try {
178
176
  const data = JSON.parse(event.data)
179
- const row = ingestJobs.value.find((j) => j.job_id === data.job_id)
177
+ const row = ingestJobs.value.find(j => j.job_id === data.job_id)
180
178
  if (!row) return
181
179
  if (data.type === 'job_progress') {
182
180
  row.progress = data.progress
@@ -192,23 +190,21 @@ function connectWebSocket() {
192
190
  }
193
191
  }
194
192
 
195
-
196
193
  function disconnectWebSocket() {
197
194
  if (ws) {
198
- try { ws.close() } catch { /* already closed */ }
195
+ try {
196
+ ws.close()
197
+ } catch { /* already closed */ }
199
198
  ws = null
200
199
  }
201
200
  }
202
201
 
203
-
204
202
  onBeforeUnmount(() => {
205
203
  disconnectWebSocket()
206
204
  })
207
205
 
208
-
209
206
  // ─── Step 3: build the persona draft ────────────────────────────────────
210
207
 
211
-
212
208
  async function runDescriptionBuild() {
213
209
  building.value = true
214
210
  buildError.value = null
@@ -221,12 +217,12 @@ async function runDescriptionBuild() {
221
217
  body: {
222
218
  name: name.value.trim(),
223
219
  description: description.value.trim(),
224
- source_label: sourceLabel.value.trim() || name.value.trim(),
225
- },
226
- },
220
+ source_label: sourceLabel.value.trim() || name.value.trim()
221
+ }
222
+ }
227
223
  )
228
- if ('error' in res && typeof (res as any).error === 'string') {
229
- throw new Error((res as any).error)
224
+ if (typeof res.error === 'string') {
225
+ throw new Error(res.error)
230
226
  }
231
227
  draft.value = res.persona
232
228
  chunksUsed.value = 0
@@ -238,24 +234,23 @@ async function runDescriptionBuild() {
238
234
  }
239
235
  }
240
236
 
241
-
242
237
  async function runBuild() {
243
238
  building.value = true
244
239
  buildError.value = null
245
240
  draft.value = null
246
241
  try {
247
- const res = await $fetch<{ persona: Persona, chunks_used: number, provider_name: string }>(
242
+ const res = await $fetch<{ persona: Persona, chunks_used: number, provider_name: string, error?: string }>(
248
243
  `${apiBase}/api/personas/build`,
249
244
  {
250
245
  method: 'POST',
251
246
  body: {
252
247
  name: name.value.trim(),
253
- source_label: sourceLabel.value.trim() || name.value.trim(),
254
- },
255
- },
248
+ source_label: sourceLabel.value.trim() || name.value.trim()
249
+ }
250
+ }
256
251
  )
257
- if ('error' in res && typeof (res as any).error === 'string') {
258
- throw new Error((res as any).error)
252
+ if (typeof res.error === 'string') {
253
+ throw new Error(res.error)
259
254
  }
260
255
  draft.value = res.persona
261
256
  chunksUsed.value = res.chunks_used
@@ -267,45 +262,41 @@ async function runBuild() {
267
262
  }
268
263
  }
269
264
 
270
-
271
265
  // ─── Step 4: save + optional clone ──────────────────────────────────────
272
266
 
273
-
274
267
  async function savePersona() {
275
268
  if (!draft.value) return
276
269
  saving.value = true
277
270
  try {
278
271
  const created = await $fetch<Persona>(`${apiBase}/api/personas`, {
279
272
  method: 'POST',
280
- body: draft.value,
273
+ body: draft.value
281
274
  })
282
275
  if (saveAndClone.value && cloneDept.value && cloneTier.value) {
283
276
  await $fetch(`${apiBase}/api/personas/${created.id}/clone`, {
284
277
  method: 'POST',
285
- body: { department: cloneDept.value, tier: Number(cloneTier.value) },
278
+ body: { department: cloneDept.value, tier: Number(cloneTier.value) }
286
279
  })
287
280
  }
288
281
  toast.add({
289
282
  title: saveAndClone.value ? 'Persona saved + agent cloned' : 'Persona saved',
290
283
  description: `${created.name} is now in your board.`,
291
- color: 'success',
284
+ color: 'success'
292
285
  })
293
286
  emit('completed', created)
294
287
  } catch (err) {
295
288
  toast.add({
296
289
  title: 'Save failed',
297
290
  description: err instanceof Error ? err.message : 'unknown error',
298
- color: 'error',
291
+ color: 'error'
299
292
  })
300
293
  } finally {
301
294
  saving.value = false
302
295
  }
303
296
  }
304
297
 
305
-
306
298
  // ─── Auto-advance when ingest completes ─────────────────────────────────
307
299
 
308
-
309
300
  watch(allIngestComplete, async (done) => {
310
301
  if (done && step.value === 2 && ingestCompletedCount.value > 0) {
311
302
  step.value = 3
@@ -313,13 +304,11 @@ watch(allIngestComplete, async (done) => {
313
304
  }
314
305
  })
315
306
 
316
-
317
307
  function cancel() {
318
308
  disconnectWebSocket()
319
309
  emit('cancelled')
320
310
  }
321
311
 
322
-
323
312
  function backToStep1() {
324
313
  disconnectWebSocket()
325
314
  step.value = 1
@@ -332,13 +321,15 @@ function backToStep1() {
332
321
  <template #header>
333
322
  <div class="flex items-center justify-between">
334
323
  <div>
335
- <h3 class="text-lg font-semibold">AI Persona Builder</h3>
324
+ <h3 class="text-lg font-semibold">
325
+ AI Persona Builder
326
+ </h3>
336
327
  <p class="text-sm text-muted mt-1">
337
328
  Step {{ step }} of 4 — {{ {
338
329
  1: 'Sources',
339
330
  2: 'Indexing',
340
331
  3: 'Generating DNA',
341
- 4: 'Review & save',
332
+ 4: 'Review & save'
342
333
  }[step] }}
343
334
  </p>
344
335
  </div>
@@ -385,9 +376,9 @@ function backToStep1() {
385
376
  <div class="grid grid-cols-1 md:grid-cols-3 gap-2">
386
377
  <button
387
378
  v-for="m in ([
388
- { key: 'sources', title: 'Ingest sources', desc: 'YouTube, articles, PDFs — best fidelity' },
389
- { key: 'existing', title: 'Existing chunks', desc: 'Use what is already indexed' },
390
- { key: 'description', title: 'From description', desc: 'No sources — pure description' },
379
+ { key: 'sources', title: 'Ingest sources', desc: 'YouTube, articles, PDFs — best fidelity' },
380
+ { key: 'existing', title: 'Existing chunks', desc: 'Use what is already indexed' },
381
+ { key: 'description', title: 'From description', desc: 'No sources — pure description' }
391
382
  ] as const)"
392
383
  :key="m.key"
393
384
  type="button"
@@ -395,8 +386,12 @@ function backToStep1() {
395
386
  :class="mode === m.key ? 'border-primary bg-primary/5' : 'border-default hover:border-primary/40'"
396
387
  @click="mode = m.key"
397
388
  >
398
- <p class="text-sm font-semibold">{{ m.title }}</p>
399
- <p class="text-xs text-muted mt-1">{{ m.desc }}</p>
389
+ <p class="text-sm font-semibold">
390
+ {{ m.title }}
391
+ </p>
392
+ <p class="text-xs text-muted mt-1">
393
+ {{ m.desc }}
394
+ </p>
400
395
  </button>
401
396
  </div>
402
397
  </UFormField>
@@ -425,7 +420,7 @@ function backToStep1() {
425
420
  :items="archetypes.map((a) => ({
426
421
  label: `${a.name} — ${a.title}`,
427
422
  icon: 'i-lucide-sparkles',
428
- onSelect: () => applyArchetype(a),
423
+ onSelect: () => applyArchetype(a)
429
424
  }))"
430
425
  >
431
426
  <UButton
@@ -466,7 +461,7 @@ function backToStep1() {
466
461
  :label="(
467
462
  mode === 'sources' ? `Index ${sourceLineCount} source${sourceLineCount === 1 ? '' : 's'} & build`
468
463
  : mode === 'existing' ? 'Generate from existing knowledge'
469
- : 'Generate from description'
464
+ : 'Generate from description'
470
465
  )"
471
466
  icon="i-lucide-arrow-right"
472
467
  :disabled="(
@@ -501,18 +496,20 @@ function backToStep1() {
501
496
  queued: 'i-lucide-clock',
502
497
  processing: 'i-lucide-loader-2 animate-spin',
503
498
  completed: 'i-lucide-check-circle',
504
- failed: 'i-lucide-x-circle',
499
+ failed: 'i-lucide-x-circle'
505
500
  }[job.status]"
506
501
  :class="{
507
502
  queued: 'text-muted',
508
503
  processing: 'text-primary',
509
504
  completed: 'text-green-500',
510
- failed: 'text-red-500',
505
+ failed: 'text-red-500'
511
506
  }[job.status]"
512
507
  class="size-4 shrink-0"
513
508
  />
514
509
  <div class="flex-1 min-w-0">
515
- <p class="text-sm font-mono truncate">{{ job.source }}</p>
510
+ <p class="text-sm font-mono truncate">
511
+ {{ job.source }}
512
+ </p>
516
513
  <UProgress
517
514
  v-if="job.status === 'processing' || job.status === 'queued'"
518
515
  :value="job.progress"
@@ -520,7 +517,9 @@ function backToStep1() {
520
517
  size="xs"
521
518
  class="mt-1"
522
519
  />
523
- <p v-if="job.error" class="text-xs text-red-400 mt-1">{{ job.error }}</p>
520
+ <p v-if="job.error" class="text-xs text-red-400 mt-1">
521
+ {{ job.error }}
522
+ </p>
524
523
  </div>
525
524
  <span class="text-xs text-muted">{{ job.progress }}%</span>
526
525
  </div>
@@ -543,13 +542,27 @@ function backToStep1() {
543
542
  <div class="flex items-start gap-3">
544
543
  <UIcon name="i-lucide-alert-circle" class="size-5 text-red-500 mt-0.5 shrink-0" />
545
544
  <div class="flex-1">
546
- <p class="text-sm font-medium text-red-400">Build failed</p>
547
- <p class="text-xs text-muted mt-1">{{ buildError }}</p>
545
+ <p class="text-sm font-medium text-red-400">
546
+ Build failed
547
+ </p>
548
+ <p class="text-xs text-muted mt-1">
549
+ {{ buildError }}
550
+ </p>
548
551
  </div>
549
552
  </div>
550
553
  <div class="flex gap-2 mt-3">
551
- <UButton label="Retry" variant="outline" size="sm" @click="runBuild" />
552
- <UButton label="Back to sources" variant="ghost" size="sm" @click="backToStep1" />
554
+ <UButton
555
+ label="Retry"
556
+ variant="outline"
557
+ size="sm"
558
+ @click="runBuild"
559
+ />
560
+ <UButton
561
+ label="Back to sources"
562
+ variant="ghost"
563
+ size="sm"
564
+ @click="backToStep1"
565
+ />
553
566
  </div>
554
567
  </div>
555
568
  </div>
@@ -564,7 +577,9 @@ function backToStep1() {
564
577
  </div>
565
578
 
566
579
  <fieldset class="space-y-3">
567
- <legend class="text-xs font-bold uppercase tracking-widest text-muted mb-2">Identity</legend>
580
+ <legend class="text-xs font-bold uppercase tracking-widest text-muted mb-2">
581
+ Identity
582
+ </legend>
568
583
  <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
569
584
  <UFormField label="Name">
570
585
  <UInput v-model="draft.name" class="w-full" />
@@ -579,7 +594,9 @@ function backToStep1() {
579
594
  </fieldset>
580
595
 
581
596
  <fieldset class="space-y-3">
582
- <legend class="text-xs font-bold uppercase tracking-widest text-muted mb-2">Behavioural DNA</legend>
597
+ <legend class="text-xs font-bold uppercase tracking-widest text-muted mb-2">
598
+ Behavioural DNA
599
+ </legend>
583
600
  <div class="grid grid-cols-2 md:grid-cols-4 gap-3">
584
601
  <UFormField label="MBTI">
585
602
  <UInput v-model="draft.mbti" class="w-full" />
@@ -588,34 +605,50 @@ function backToStep1() {
588
605
  <UInput v-model="draft.disc.primary" class="w-full" />
589
606
  </UFormField>
590
607
  <UFormField label="Enneagram type">
591
- <UInput v-model.number="draft.enneagram.type" type="number" :min="1" :max="9" class="w-full" />
608
+ <UInput
609
+ v-model.number="draft.enneagram.type"
610
+ type="number"
611
+ :min="1"
612
+ :max="9"
613
+ class="w-full"
614
+ />
592
615
  </UFormField>
593
616
  <UFormField label="Enneagram wing">
594
- <UInput v-model.number="draft.enneagram.wing" type="number" :min="1" :max="9" class="w-full" />
617
+ <UInput
618
+ v-model.number="draft.enneagram.wing"
619
+ type="number"
620
+ :min="1"
621
+ :max="9"
622
+ class="w-full"
623
+ />
595
624
  </UFormField>
596
625
  </div>
597
626
  </fieldset>
598
627
 
599
628
  <fieldset class="space-y-3">
600
- <legend class="text-xs font-bold uppercase tracking-widest text-muted mb-2">Knowledge</legend>
629
+ <legend class="text-xs font-bold uppercase tracking-widest text-muted mb-2">
630
+ Knowledge
631
+ </legend>
601
632
  <UFormField label="Mental models" help="comma-separated">
602
633
  <UInput
603
634
  :model-value="draft.mental_models.join(', ')"
604
- @update:model-value="(v: string) => draft && (draft.mental_models = v.split(',').map(s => s.trim()).filter(Boolean))"
605
635
  class="w-full"
636
+ @update:model-value="(v: string) => draft && (draft.mental_models = v.split(',').map(s => s.trim()).filter(Boolean))"
606
637
  />
607
638
  </UFormField>
608
639
  <UFormField label="Expertise domains" help="comma-separated">
609
640
  <UInput
610
641
  :model-value="draft.expertise_domains.join(', ')"
611
- @update:model-value="(v: string) => draft && (draft.expertise_domains = v.split(',').map(s => s.trim()).filter(Boolean))"
612
642
  class="w-full"
643
+ @update:model-value="(v: string) => draft && (draft.expertise_domains = v.split(',').map(s => s.trim()).filter(Boolean))"
613
644
  />
614
645
  </UFormField>
615
646
  </fieldset>
616
647
 
617
648
  <fieldset class="space-y-3">
618
- <legend class="text-xs font-bold uppercase tracking-widest text-muted mb-2">Save options</legend>
649
+ <legend class="text-xs font-bold uppercase tracking-widest text-muted mb-2">
650
+ Save options
651
+ </legend>
619
652
  <UCheckbox
620
653
  v-model="saveAndClone"
621
654
  label="Also clone to an agent immediately"