arkaos 2.99.0 → 3.0.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.99.0
1
+ 3.0.0
@@ -167,6 +167,14 @@ function goToAgent(id: string) {
167
167
  <template #trailing>
168
168
  <UBadge v-if="data?.total" :label="data.total" variant="subtle" />
169
169
  </template>
170
+ <template #right>
171
+ <UButton
172
+ label="New Agent"
173
+ icon="i-lucide-plus"
174
+ size="sm"
175
+ to="/agents/new"
176
+ />
177
+ </template>
170
178
  </UDashboardNavbar>
171
179
  </template>
172
180
 
@@ -0,0 +1,478 @@
1
+ <script setup lang="ts">
2
+ // PR82a v3.0.0 — /agents/new manual create page.
3
+ //
4
+ // Single-page form (sections, no multi-step) that mirrors the safe-to-edit
5
+ // fields from AgentEditDrawer but in "create" mode:
6
+ // - Identity (name, role, department, tier)
7
+ // - Behavioural DNA (DISC + Enneagram + MBTI + Big Five, with sensible
8
+ // defaults — operator can edit)
9
+ // - Knowledge (mental models, expertise domains, frameworks)
10
+ // - Communication (tone, vocab, format, language, avoid)
11
+ //
12
+ // AI-assist (PR81) is wired on the three list fields so a draft agent
13
+ // can be filled with one click. On Save → POST /api/agents → navigate
14
+ // to /agents/{slug}.
15
+
16
+ import type { Persona } from '~/types'
17
+
18
+ const { fetchApi, apiBase } = useApi()
19
+ const toast = useToast()
20
+
21
+ const { data: personasData } = fetchApi<{ personas: Persona[] }>('/api/personas')
22
+ const personaOptions = computed(() =>
23
+ (personasData.value?.personas ?? []).map((p) => ({
24
+ label: p.name + (p.title ? ` — ${p.title}` : ''),
25
+ value: p.id,
26
+ })),
27
+ )
28
+
29
+ interface AgentDraft {
30
+ name: string
31
+ role: string
32
+ department: string
33
+ tier: number
34
+ disc_primary: string
35
+ disc_secondary: string
36
+ enneagram_type: number
37
+ enneagram_wing: number
38
+ mbti: string
39
+ big_five: {
40
+ openness: number
41
+ conscientiousness: number
42
+ extraversion: number
43
+ agreeableness: number
44
+ neuroticism: number
45
+ }
46
+ mental_models_primary: string[]
47
+ expertise_domains: string[]
48
+ expertise_depth: string
49
+ expertise_years: number
50
+ frameworks: string[]
51
+ comm_tone: string
52
+ comm_vocab: string
53
+ comm_format: string
54
+ comm_language: string
55
+ comm_avoid: string[]
56
+ linked_personas: string[]
57
+ }
58
+
59
+ const draft = ref<AgentDraft>({
60
+ name: '',
61
+ role: '',
62
+ department: 'dev',
63
+ tier: 2,
64
+ disc_primary: 'I',
65
+ disc_secondary: 'S',
66
+ enneagram_type: 5,
67
+ enneagram_wing: 4,
68
+ mbti: 'INTJ',
69
+ big_five: {
70
+ openness: 70,
71
+ conscientiousness: 70,
72
+ extraversion: 50,
73
+ agreeableness: 60,
74
+ neuroticism: 30,
75
+ },
76
+ mental_models_primary: [],
77
+ expertise_domains: [],
78
+ expertise_depth: 'advanced',
79
+ expertise_years: 5,
80
+ frameworks: [],
81
+ comm_tone: '',
82
+ comm_vocab: 'specialist',
83
+ comm_format: '',
84
+ comm_language: 'en',
85
+ comm_avoid: [],
86
+ linked_personas: [],
87
+ })
88
+
89
+ const saving = ref(false)
90
+
91
+ const departmentOptions = [
92
+ 'dev', 'marketing', 'brand', 'finance', 'strategy', 'ecom', 'kb', 'ops',
93
+ 'pm', 'saas', 'landing', 'content', 'community', 'sales', 'leadership', 'org',
94
+ ].map((d) => ({ label: d, value: d }))
95
+
96
+ const tierOptions = [
97
+ { label: 'Tier 1 — Squad Lead', value: 1 },
98
+ { label: 'Tier 2 — Specialist', value: 2 },
99
+ { label: 'Tier 3 — Support', value: 3 },
100
+ ]
101
+ const discOptions = ['D', 'I', 'S', 'C'].map((v) => ({ label: v, value: v }))
102
+ const depthOptions = [
103
+ { label: 'Intermediate', value: 'intermediate' },
104
+ { label: 'Advanced', value: 'advanced' },
105
+ { label: 'Expert', value: 'expert' },
106
+ { label: 'Master', value: 'master' },
107
+ ]
108
+ const vocabOptions = [
109
+ { label: 'Lay (no jargon)', value: 'lay' },
110
+ { label: 'Specialist (industry terms)', value: 'specialist' },
111
+ { label: 'Expert (research-level)', value: 'expert' },
112
+ ]
113
+ const mbtiOptions = [
114
+ 'INTJ', 'INTP', 'ENTJ', 'ENTP',
115
+ 'INFJ', 'INFP', 'ENFJ', 'ENFP',
116
+ 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ',
117
+ 'ISTP', 'ISFP', 'ESTP', 'ESFP',
118
+ ].map((t) => ({ label: t, value: t }))
119
+
120
+ function listToCsv(list: string[]): string {
121
+ return list.join(', ')
122
+ }
123
+ function csvToList(value: string): string[] {
124
+ return value.split(',').map((s) => s.trim()).filter(Boolean)
125
+ }
126
+
127
+ // PR81 suggest wiring — three list fields.
128
+ type SuggestField = 'mental_models' | 'frameworks' | 'expertise_domains'
129
+ const suggestingField = ref<SuggestField | null>(null)
130
+
131
+ async function suggest(field: SuggestField) {
132
+ const current
133
+ = field === 'mental_models'
134
+ ? draft.value.mental_models_primary
135
+ : field === 'frameworks'
136
+ ? draft.value.frameworks
137
+ : draft.value.expertise_domains
138
+ if (!draft.value.name.trim() || !draft.value.role.trim()) {
139
+ toast.add({
140
+ title: 'Add a name and role first',
141
+ description: 'AI needs the basics to make useful suggestions.',
142
+ color: 'warning',
143
+ })
144
+ return
145
+ }
146
+ suggestingField.value = field
147
+ try {
148
+ const res = await $fetch<{
149
+ suggestions: string[]
150
+ provider_name: string
151
+ error?: string
152
+ }>(`${apiBase}/api/agents/suggest`, {
153
+ method: 'POST',
154
+ body: {
155
+ field,
156
+ count: 5,
157
+ context: {
158
+ name: draft.value.name,
159
+ role: draft.value.role,
160
+ department: draft.value.department,
161
+ current,
162
+ },
163
+ },
164
+ })
165
+ if (res.error) throw new Error(res.error)
166
+ const additions = (res.suggestions ?? []).filter(
167
+ (s) => !current.some((c) => c.toLowerCase() === s.toLowerCase()),
168
+ )
169
+ if (additions.length === 0) {
170
+ toast.add({ title: 'No new suggestions', color: 'info' })
171
+ return
172
+ }
173
+ const merged = [...current, ...additions]
174
+ if (field === 'mental_models') draft.value.mental_models_primary = merged
175
+ else if (field === 'frameworks') draft.value.frameworks = merged
176
+ else draft.value.expertise_domains = merged
177
+ toast.add({
178
+ title: `Added ${additions.length} suggestion${additions.length === 1 ? '' : 's'}`,
179
+ description: `via ${res.provider_name}`,
180
+ color: 'success',
181
+ icon: 'i-lucide-sparkles',
182
+ })
183
+ } catch (err) {
184
+ toast.add({
185
+ title: 'Suggestion failed',
186
+ description: err instanceof Error ? err.message : 'unknown error',
187
+ color: 'error',
188
+ })
189
+ } finally {
190
+ suggestingField.value = null
191
+ }
192
+ }
193
+
194
+ const canSave = computed(() => {
195
+ return (
196
+ draft.value.name.trim().length > 0
197
+ && draft.value.role.trim().length > 0
198
+ && draft.value.department.trim().length > 0
199
+ && draft.value.disc_primary !== draft.value.disc_secondary
200
+ )
201
+ })
202
+
203
+ async function save() {
204
+ if (!canSave.value) return
205
+ saving.value = true
206
+ try {
207
+ const body = {
208
+ name: draft.value.name.trim(),
209
+ role: draft.value.role.trim(),
210
+ department: draft.value.department,
211
+ tier: draft.value.tier,
212
+ behavioral_dna: {
213
+ disc: {
214
+ primary: draft.value.disc_primary,
215
+ secondary: draft.value.disc_secondary,
216
+ },
217
+ enneagram: {
218
+ type: draft.value.enneagram_type,
219
+ wing: draft.value.enneagram_wing,
220
+ },
221
+ mbti: draft.value.mbti,
222
+ big_five: draft.value.big_five,
223
+ },
224
+ mental_models: { primary: draft.value.mental_models_primary, secondary: [] },
225
+ expertise: {
226
+ domains: draft.value.expertise_domains,
227
+ frameworks: draft.value.frameworks,
228
+ depth: draft.value.expertise_depth,
229
+ years_equivalent: draft.value.expertise_years,
230
+ },
231
+ communication: {
232
+ tone: draft.value.comm_tone,
233
+ vocabulary_level: draft.value.comm_vocab,
234
+ preferred_format: draft.value.comm_format,
235
+ language: draft.value.comm_language,
236
+ avoid: draft.value.comm_avoid,
237
+ },
238
+ linked_personas: draft.value.linked_personas,
239
+ }
240
+ const res = await $fetch<{
241
+ id: string
242
+ created: boolean
243
+ yaml_path?: string
244
+ error?: string
245
+ }>(`${apiBase}/api/agents`, { method: 'POST', body })
246
+ if (res.error) throw new Error(res.error)
247
+ toast.add({
248
+ title: 'Agent created',
249
+ description: res.yaml_path?.split('/').slice(-3).join('/') ?? res.id,
250
+ color: 'success',
251
+ })
252
+ navigateTo(`/agents/${res.id}`)
253
+ } catch (err) {
254
+ toast.add({
255
+ title: 'Create failed',
256
+ description: err instanceof Error ? err.message : 'unknown error',
257
+ color: 'error',
258
+ })
259
+ } finally {
260
+ saving.value = false
261
+ }
262
+ }
263
+
264
+ const bigFiveLabels: Record<string, string> = {
265
+ openness: 'Openness',
266
+ conscientiousness: 'Conscientiousness',
267
+ extraversion: 'Extraversion',
268
+ agreeableness: 'Agreeableness',
269
+ neuroticism: 'Neuroticism',
270
+ }
271
+ const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeableness', 'neuroticism'] as const
272
+ </script>
273
+
274
+ <template>
275
+ <UDashboardPanel id="agents-new">
276
+ <template #header>
277
+ <UDashboardNavbar title="New Agent">
278
+ <template #leading>
279
+ <UButton
280
+ icon="i-lucide-arrow-left"
281
+ variant="ghost"
282
+ size="sm"
283
+ aria-label="Back to agents"
284
+ to="/agents"
285
+ />
286
+ </template>
287
+ <template #trailing>
288
+ <UBadge
289
+ label="AI-assisted"
290
+ icon="i-lucide-sparkles"
291
+ color="primary"
292
+ variant="soft"
293
+ size="sm"
294
+ />
295
+ </template>
296
+ </UDashboardNavbar>
297
+ </template>
298
+
299
+ <template #body>
300
+ <div class="max-w-4xl mx-auto py-2 space-y-6">
301
+ <section class="space-y-3">
302
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Identity</h3>
303
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
304
+ <UFormField label="Name" required>
305
+ <UInput v-model="draft.name" class="w-full" placeholder="Lucas" />
306
+ </UFormField>
307
+ <UFormField label="Role" required>
308
+ <UInput v-model="draft.role" class="w-full" placeholder="Market & Competitive Intelligence Analyst" />
309
+ </UFormField>
310
+ <UFormField label="Department" required>
311
+ <USelect v-model="draft.department" :items="departmentOptions" class="w-full" />
312
+ </UFormField>
313
+ <UFormField label="Tier">
314
+ <USelect v-model="draft.tier" :items="tierOptions" class="w-full" />
315
+ </UFormField>
316
+ </div>
317
+ </section>
318
+
319
+ <section class="space-y-3">
320
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Behavioural DNA</h3>
321
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-3">
322
+ <UFormField label="DISC primary">
323
+ <USelect v-model="draft.disc_primary" :items="discOptions" class="w-full" />
324
+ </UFormField>
325
+ <UFormField label="DISC secondary">
326
+ <USelect v-model="draft.disc_secondary" :items="discOptions" class="w-full" />
327
+ </UFormField>
328
+ <UFormField label="Enneagram type">
329
+ <UInput v-model.number="draft.enneagram_type" type="number" :min="1" :max="9" class="w-full" />
330
+ </UFormField>
331
+ <UFormField label="Enneagram wing">
332
+ <UInput v-model.number="draft.enneagram_wing" type="number" :min="1" :max="9" class="w-full" />
333
+ </UFormField>
334
+ <UFormField label="MBTI">
335
+ <USelect v-model="draft.mbti" :items="mbtiOptions" class="w-full" />
336
+ </UFormField>
337
+ </div>
338
+ <p v-if="draft.disc_primary === draft.disc_secondary" class="text-xs text-error">
339
+ DISC primary and secondary must differ.
340
+ </p>
341
+ <div class="space-y-2">
342
+ <p class="text-sm font-semibold text-muted">Big Five (OCEAN)</p>
343
+ <div v-for="key in bigFiveKeys" :key="key" class="flex items-center gap-3">
344
+ <span class="w-40 text-sm text-muted">{{ bigFiveLabels[key] }}</span>
345
+ <UInput
346
+ v-model.number="draft.big_five[key]"
347
+ type="number"
348
+ :min="0"
349
+ :max="100"
350
+ class="w-20"
351
+ />
352
+ <div class="flex-1 h-2 rounded-full bg-muted/20">
353
+ <div class="h-2 rounded-full bg-primary" :style="{ width: `${draft.big_five[key]}%` }" />
354
+ </div>
355
+ </div>
356
+ </div>
357
+ </section>
358
+
359
+ <section class="space-y-3">
360
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Knowledge</h3>
361
+ <UFormField label="Mental models (primary)" help="comma-separated">
362
+ <template #hint>
363
+ <UButton
364
+ label="Suggest with AI"
365
+ icon="i-lucide-sparkles"
366
+ size="xs"
367
+ color="primary"
368
+ variant="soft"
369
+ :loading="suggestingField === 'mental_models'"
370
+ :disabled="suggestingField !== null"
371
+ @click="suggest('mental_models')"
372
+ />
373
+ </template>
374
+ <UInput
375
+ :model-value="listToCsv(draft.mental_models_primary)"
376
+ @update:model-value="(v: string) => { draft.mental_models_primary = csvToList(v) }"
377
+ class="w-full"
378
+ />
379
+ </UFormField>
380
+ <UFormField label="Expertise domains" help="comma-separated">
381
+ <template #hint>
382
+ <UButton
383
+ label="Suggest with AI"
384
+ icon="i-lucide-sparkles"
385
+ size="xs"
386
+ color="primary"
387
+ variant="soft"
388
+ :loading="suggestingField === 'expertise_domains'"
389
+ :disabled="suggestingField !== null"
390
+ @click="suggest('expertise_domains')"
391
+ />
392
+ </template>
393
+ <UInput
394
+ :model-value="listToCsv(draft.expertise_domains)"
395
+ @update:model-value="(v: string) => { draft.expertise_domains = csvToList(v) }"
396
+ class="w-full"
397
+ />
398
+ </UFormField>
399
+ <div class="grid grid-cols-2 gap-3">
400
+ <UFormField label="Depth">
401
+ <USelect v-model="draft.expertise_depth" :items="depthOptions" class="w-full" />
402
+ </UFormField>
403
+ <UFormField label="Years (equivalent)">
404
+ <UInput v-model.number="draft.expertise_years" type="number" :min="0" :max="60" class="w-full" />
405
+ </UFormField>
406
+ </div>
407
+ <UFormField label="Frameworks" help="comma-separated">
408
+ <template #hint>
409
+ <UButton
410
+ label="Suggest with AI"
411
+ icon="i-lucide-sparkles"
412
+ size="xs"
413
+ color="primary"
414
+ variant="soft"
415
+ :loading="suggestingField === 'frameworks'"
416
+ :disabled="suggestingField !== null"
417
+ @click="suggest('frameworks')"
418
+ />
419
+ </template>
420
+ <UInput
421
+ :model-value="listToCsv(draft.frameworks)"
422
+ @update:model-value="(v: string) => { draft.frameworks = csvToList(v) }"
423
+ class="w-full"
424
+ />
425
+ </UFormField>
426
+ </section>
427
+
428
+ <section class="space-y-3">
429
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Communication</h3>
430
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
431
+ <UFormField label="Tone">
432
+ <UInput v-model="draft.comm_tone" class="w-full" placeholder="Analytical, calm" />
433
+ </UFormField>
434
+ <UFormField label="Vocabulary level">
435
+ <USelect v-model="draft.comm_vocab" :items="vocabOptions" class="w-full" />
436
+ </UFormField>
437
+ <UFormField label="Preferred format">
438
+ <UInput v-model="draft.comm_format" class="w-full" placeholder="Briefs, tables, charts" />
439
+ </UFormField>
440
+ <UFormField label="Language">
441
+ <UInput v-model="draft.comm_language" class="w-full" placeholder="en" />
442
+ </UFormField>
443
+ </div>
444
+ <UFormField label="Avoid (phrases)" help="comma-separated">
445
+ <UInput
446
+ :model-value="listToCsv(draft.comm_avoid)"
447
+ @update:model-value="(v: string) => { draft.comm_avoid = csvToList(v) }"
448
+ class="w-full"
449
+ />
450
+ </UFormField>
451
+ </section>
452
+
453
+ <section class="space-y-3">
454
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted">Linked personas</h3>
455
+ <USelectMenu
456
+ v-model="draft.linked_personas"
457
+ :items="personaOptions"
458
+ value-key="value"
459
+ multiple
460
+ placeholder="Select personas to link"
461
+ class="w-full"
462
+ />
463
+ </section>
464
+
465
+ <div class="flex items-center justify-end gap-2 pt-4 border-t border-default">
466
+ <UButton label="Cancel" variant="ghost" :disabled="saving" to="/agents" />
467
+ <UButton
468
+ label="Create agent"
469
+ icon="i-lucide-check"
470
+ :loading="saving"
471
+ :disabled="!canSave"
472
+ @click="save"
473
+ />
474
+ </div>
475
+ </div>
476
+ </template>
477
+ </UDashboardPanel>
478
+ </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "2.99.0",
3
+ "version": "3.0.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.99.0"
3
+ version = "3.0.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"}
@@ -1683,6 +1683,150 @@ def metrics():
1683
1683
  return {"entries": entries[-50:], "avg_ms": round(avg_ms, 1), "total_calls": len(entries)}
1684
1684
 
1685
1685
 
1686
+ # --- Agent create (PR82 v3.0.0) ---
1687
+
1688
+ @app.post("/api/agents")
1689
+ def agent_create(body: dict):
1690
+ """Create a new agent YAML file from a manual draft.
1691
+
1692
+ Required body keys: name, role, department, tier.
1693
+ Optional: behavioral_dna, expertise, mental_models, communication,
1694
+ linked_personas, authority.
1695
+
1696
+ Slug rule: <name-kebab>-<random-suffix> when no explicit `id` is
1697
+ given. The endpoint refuses to overwrite an existing file.
1698
+ """
1699
+ if not isinstance(body, dict):
1700
+ return {"error": "body must be an object"}
1701
+ return _do_agent_create(body)
1702
+
1703
+
1704
+ def _do_agent_create(body: dict) -> dict:
1705
+ import re
1706
+ import uuid
1707
+
1708
+ name = (body.get("name") or "").strip()
1709
+ role = (body.get("role") or "").strip()
1710
+ department = (body.get("department") or "").strip().lower()
1711
+ tier_raw = body.get("tier")
1712
+ if not name or not role or not department:
1713
+ return {"error": "name, role, and department are required"}
1714
+ try:
1715
+ tier = int(tier_raw) if tier_raw is not None else 2
1716
+ except (TypeError, ValueError):
1717
+ return {"error": "tier must be an integer"}
1718
+
1719
+ dept_dir = ARKAOS_ROOT / "departments" / department / "agents"
1720
+ if not dept_dir.exists():
1721
+ return {"error": f"department '{department}' not found"}
1722
+
1723
+ explicit_id = (body.get("id") or "").strip()
1724
+ if explicit_id:
1725
+ slug = _agent_slugify(explicit_id)
1726
+ else:
1727
+ slug = f"{_agent_slugify(name)}-{uuid.uuid4().hex[:6]}"
1728
+ yaml_file = dept_dir / f"{slug}.yaml"
1729
+ if yaml_file.exists():
1730
+ return {"error": f"agent with id '{slug}' already exists"}
1731
+
1732
+ try:
1733
+ import yaml as _yaml
1734
+ except ImportError:
1735
+ return {"error": "PyYAML unavailable"}
1736
+
1737
+ payload = _build_agent_yaml(slug, name, role, department, tier, body)
1738
+ try:
1739
+ tmp = yaml_file.with_suffix(yaml_file.suffix + ".tmp")
1740
+ tmp.write_text(
1741
+ _yaml.safe_dump(payload, sort_keys=False, allow_unicode=True, default_flow_style=False),
1742
+ encoding="utf-8",
1743
+ )
1744
+ tmp.replace(yaml_file)
1745
+ except OSError as exc:
1746
+ return {"error": f"write failed: {exc}"}
1747
+ return {"id": slug, "created": True, "yaml_path": str(yaml_file)}
1748
+
1749
+
1750
+ def _agent_slugify(text: str) -> str:
1751
+ import re
1752
+ cleaned = re.sub(r"[^a-z0-9-]+", "-", text.lower())
1753
+ cleaned = re.sub(r"-+", "-", cleaned).strip("-")
1754
+ return cleaned or "agent"
1755
+
1756
+
1757
+ def _build_agent_yaml(
1758
+ slug: str, name: str, role: str, department: str, tier: int, body: dict,
1759
+ ) -> dict:
1760
+ """Compose the YAML payload, applying sensible defaults."""
1761
+ dna = body.get("behavioral_dna") or {}
1762
+ disc = dna.get("disc") or {}
1763
+ enneagram = dna.get("enneagram") or {}
1764
+ big_five = dna.get("big_five") or {}
1765
+ mbti_raw = dna.get("mbti")
1766
+ mbti = mbti_raw.get("type") if isinstance(mbti_raw, dict) else mbti_raw
1767
+
1768
+ expertise = body.get("expertise") or {}
1769
+ mental_models = body.get("mental_models") or {}
1770
+ communication = body.get("communication") or {}
1771
+ authority = body.get("authority") or {}
1772
+
1773
+ payload: dict = {
1774
+ "id": slug,
1775
+ "name": name,
1776
+ "role": role,
1777
+ "department": department,
1778
+ "tier": tier,
1779
+ "model": "opus" if tier == 0 else "sonnet",
1780
+ "behavioral_dna": {
1781
+ "disc": {
1782
+ "primary": (disc.get("primary") or "I").upper(),
1783
+ "secondary": (disc.get("secondary") or "S").upper(),
1784
+ "communication_style": disc.get("communication_style") or "",
1785
+ "under_pressure": disc.get("under_pressure") or "",
1786
+ "motivator": disc.get("motivator") or "",
1787
+ },
1788
+ "enneagram": {
1789
+ "type": int(enneagram.get("type") or 5),
1790
+ "wing": int(enneagram.get("wing") or 4),
1791
+ "core_motivation": enneagram.get("core_motivation") or "",
1792
+ "core_fear": enneagram.get("core_fear") or "",
1793
+ "subtype": enneagram.get("subtype") or "self-preservation",
1794
+ },
1795
+ "big_five": {
1796
+ "openness": int(big_five.get("openness") or 70),
1797
+ "conscientiousness": int(big_five.get("conscientiousness") or 70),
1798
+ "extraversion": int(big_five.get("extraversion") or 50),
1799
+ "agreeableness": int(big_five.get("agreeableness") or 60),
1800
+ "neuroticism": int(big_five.get("neuroticism") or 30),
1801
+ },
1802
+ "mbti": {"type": (mbti or "INTJ").upper()},
1803
+ },
1804
+ "authority": {
1805
+ "delegates_to": _agent_str_list(authority.get("delegates_to") or []),
1806
+ "escalates_to": authority.get("escalates_to") or "",
1807
+ },
1808
+ "expertise": {
1809
+ "domains": _agent_str_list(expertise.get("domains") or []),
1810
+ "frameworks": _agent_str_list(expertise.get("frameworks") or []),
1811
+ "depth": expertise.get("depth") or "advanced",
1812
+ "years_equivalent": int(expertise.get("years_equivalent") or 5),
1813
+ },
1814
+ "mental_models": {
1815
+ "primary": _agent_str_list(mental_models.get("primary") or []),
1816
+ "secondary": _agent_str_list(mental_models.get("secondary") or []),
1817
+ },
1818
+ "communication": {
1819
+ "tone": communication.get("tone") or "",
1820
+ "vocabulary_level": communication.get("vocabulary_level") or "specialist",
1821
+ "preferred_format": communication.get("preferred_format") or "",
1822
+ "language": communication.get("language") or "en",
1823
+ "avoid": _agent_str_list(communication.get("avoid") or []),
1824
+ },
1825
+ "linked_personas": _agent_str_list(body.get("linked_personas") or []),
1826
+ }
1827
+ return payload
1828
+
1829
+
1686
1830
  # --- AI list-field suggester (PR81 v2.99.0) ---
1687
1831
 
1688
1832
  @app.post("/api/agents/suggest")