arkaos 2.77.0 → 2.79.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/core/runtime/__pycache__/codex_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/update_orchestrator.cpython-313.pyc +0 -0
- package/core/sync/update_orchestrator.py +244 -0
- package/dashboard/app/components/PersonaWizard.vue +504 -0
- package/dashboard/app/pages/personas.vue +55 -7
- package/departments/ops/skills/update/SKILL.md +20 -4
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.79.0
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""One-stop /arka update orchestrator (PR61 v2.78.0).
|
|
2
|
+
|
|
3
|
+
The published 2-step process (`npx arkaos@latest update` then
|
|
4
|
+
`/arka update`) is fragile in practice: operators run step 2 inside
|
|
5
|
+
Claude Code without remembering step 1, so the sync engine silently
|
|
6
|
+
runs from whichever npx cache `~/.arkaos/.repo-path` last pointed at.
|
|
7
|
+
When that cache is months old the sync becomes a no-op against
|
|
8
|
+
current versions.
|
|
9
|
+
|
|
10
|
+
This module makes `/arka update` self-sufficient:
|
|
11
|
+
|
|
12
|
+
1. Read the running ArkaOS version from `<repo>/VERSION`.
|
|
13
|
+
2. Probe the npm registry for the published latest (5s timeout,
|
|
14
|
+
1-hour cache on disk to keep repeat runs cheap).
|
|
15
|
+
3. If the running version is older than the latest, shell out to
|
|
16
|
+
`npx arkaos@latest update` and wait for it to finish before
|
|
17
|
+
touching the sync engine. The npx step rewrites
|
|
18
|
+
``~/.arkaos/.repo-path`` to the freshly-extracted package so the
|
|
19
|
+
sync engine below reads the right code.
|
|
20
|
+
4. Re-read VERSION (now updated) and dispatch to ``run_sync``.
|
|
21
|
+
|
|
22
|
+
The orchestrator NEVER raises on transient failures — npm offline,
|
|
23
|
+
slow registry, missing `npx` — it logs and falls through to the sync
|
|
24
|
+
engine using whatever code is currently installed. Worst case the
|
|
25
|
+
operator sees the same behaviour as before PR61.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
import time
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Optional
|
|
37
|
+
|
|
38
|
+
from core.sync.engine import (
|
|
39
|
+
_read_current_version,
|
|
40
|
+
_read_repo_path,
|
|
41
|
+
run_sync,
|
|
42
|
+
)
|
|
43
|
+
from core.sync.schema import SyncReport
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Cache the npm-view result for an hour so repeated /arka update calls
|
|
47
|
+
# inside the same session don't re-hit the registry.
|
|
48
|
+
_NPM_CACHE_TTL_SECONDS = 3600
|
|
49
|
+
_NPM_TIMEOUT_SECONDS = 5
|
|
50
|
+
_NPM_PROBE_CMD = ("npm", "view", "arkaos", "version")
|
|
51
|
+
_NPX_UPDATE_CMD = ("npx", "-y", "arkaos@latest", "update")
|
|
52
|
+
_NPX_TIMEOUT_SECONDS = 600 # 10 minutes — large installs can be slow
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def orchestrate(
|
|
56
|
+
arkaos_home: Path,
|
|
57
|
+
skills_dir: Path,
|
|
58
|
+
home_path: str,
|
|
59
|
+
*,
|
|
60
|
+
npm_probe=None,
|
|
61
|
+
npx_run=None,
|
|
62
|
+
cache_path: Path | None = None,
|
|
63
|
+
) -> tuple[Optional[str], Optional[str], SyncReport]:
|
|
64
|
+
"""Run npm-side update when stale, then the sync engine.
|
|
65
|
+
|
|
66
|
+
Returns ``(installed_version_before, latest_version_seen, report)``.
|
|
67
|
+
The first two are None when probing failed; the third is always a
|
|
68
|
+
SyncReport (the engine itself never raises on individual project
|
|
69
|
+
failures).
|
|
70
|
+
"""
|
|
71
|
+
probe = npm_probe or _probe_npm_latest
|
|
72
|
+
runner = npx_run or _run_npx_update
|
|
73
|
+
cache = cache_path or (arkaos_home / "npm-latest.cache.json")
|
|
74
|
+
|
|
75
|
+
installed = _safe_read_version(arkaos_home)
|
|
76
|
+
latest = probe(cache)
|
|
77
|
+
|
|
78
|
+
if installed and latest and _is_older(installed, latest):
|
|
79
|
+
runner(arkaos_home)
|
|
80
|
+
|
|
81
|
+
report = run_sync(
|
|
82
|
+
arkaos_home=arkaos_home,
|
|
83
|
+
skills_dir=skills_dir,
|
|
84
|
+
home_path=home_path,
|
|
85
|
+
)
|
|
86
|
+
return installed, latest, report
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _safe_read_version(arkaos_home: Path) -> Optional[str]:
|
|
90
|
+
try:
|
|
91
|
+
v = _read_current_version(arkaos_home)
|
|
92
|
+
return v if v and v != "unknown" else None
|
|
93
|
+
except Exception: # noqa: BLE001 — never break the orchestrator
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _probe_npm_latest(cache_path: Path) -> Optional[str]:
|
|
98
|
+
"""Return the latest published arkaos version, or None on failure.
|
|
99
|
+
|
|
100
|
+
Reads from disk cache when fresh; otherwise shells out to
|
|
101
|
+
``npm view`` with a short timeout. Always swallows errors —
|
|
102
|
+
callers fall through to a no-op when probing fails.
|
|
103
|
+
"""
|
|
104
|
+
cached = _read_cache(cache_path)
|
|
105
|
+
if cached is not None:
|
|
106
|
+
return cached
|
|
107
|
+
try:
|
|
108
|
+
result = subprocess.run(
|
|
109
|
+
list(_NPM_PROBE_CMD),
|
|
110
|
+
capture_output=True,
|
|
111
|
+
text=True,
|
|
112
|
+
timeout=_NPM_TIMEOUT_SECONDS,
|
|
113
|
+
check=False,
|
|
114
|
+
)
|
|
115
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
116
|
+
return None
|
|
117
|
+
if result.returncode != 0:
|
|
118
|
+
return None
|
|
119
|
+
version = (result.stdout or "").strip()
|
|
120
|
+
if not _looks_like_semver(version):
|
|
121
|
+
return None
|
|
122
|
+
_write_cache(cache_path, version)
|
|
123
|
+
return version
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _read_cache(cache_path: Path) -> Optional[str]:
|
|
127
|
+
if not cache_path.exists():
|
|
128
|
+
return None
|
|
129
|
+
try:
|
|
130
|
+
data = json.loads(cache_path.read_text(encoding="utf-8"))
|
|
131
|
+
except (json.JSONDecodeError, OSError):
|
|
132
|
+
return None
|
|
133
|
+
if not isinstance(data, dict):
|
|
134
|
+
return None
|
|
135
|
+
ts = data.get("ts")
|
|
136
|
+
version = data.get("version")
|
|
137
|
+
if not isinstance(ts, (int, float)) or not isinstance(version, str):
|
|
138
|
+
return None
|
|
139
|
+
if time.time() - ts > _NPM_CACHE_TTL_SECONDS:
|
|
140
|
+
return None
|
|
141
|
+
return version if _looks_like_semver(version) else None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _write_cache(cache_path: Path, version: str) -> None:
|
|
145
|
+
try:
|
|
146
|
+
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
cache_path.write_text(
|
|
148
|
+
json.dumps({"version": version, "ts": time.time()}),
|
|
149
|
+
encoding="utf-8",
|
|
150
|
+
)
|
|
151
|
+
except OSError:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _looks_like_semver(value: str) -> bool:
|
|
156
|
+
if not value or len(value) > 32:
|
|
157
|
+
return False
|
|
158
|
+
# Strip any `-prerelease.suffix` so "2.77.0-beta.1" reduces to "2.77.0"
|
|
159
|
+
# before structural validation. npm canonical semver allows almost any
|
|
160
|
+
# ASCII after the dash; we don't try to validate that — we only need
|
|
161
|
+
# to confirm the leading major.minor.patch shape is intact.
|
|
162
|
+
base = value.split("-", 1)[0]
|
|
163
|
+
parts = base.split(".")
|
|
164
|
+
if len(parts) != 3:
|
|
165
|
+
return False
|
|
166
|
+
return all(part.isdigit() for part in parts)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _is_older(installed: str, latest: str) -> bool:
|
|
170
|
+
return _parse_semver(installed) < _parse_semver(latest)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _parse_semver(value: str) -> tuple[int, int, int]:
|
|
174
|
+
parts = value.split(".")
|
|
175
|
+
try:
|
|
176
|
+
major = int(parts[0])
|
|
177
|
+
minor = int(parts[1])
|
|
178
|
+
patch_str = parts[2].split("-", 1)[0]
|
|
179
|
+
patch = int(patch_str)
|
|
180
|
+
return major, minor, patch
|
|
181
|
+
except (ValueError, IndexError):
|
|
182
|
+
return (0, 0, 0)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _run_npx_update(arkaos_home: Path) -> None:
|
|
186
|
+
"""Best-effort shell-out to `npx arkaos@latest update`.
|
|
187
|
+
|
|
188
|
+
Inherits the parent's stdout/stderr so the operator sees the same
|
|
189
|
+
installer banner they would in a manual run. Swallows OSError /
|
|
190
|
+
TimeoutExpired so the surrounding sync still runs.
|
|
191
|
+
"""
|
|
192
|
+
del arkaos_home # passed for symmetry with _probe_npm_latest signature
|
|
193
|
+
try:
|
|
194
|
+
env = os.environ.copy()
|
|
195
|
+
# Some operators set CI=1 to suppress installer prompts; preserve it.
|
|
196
|
+
subprocess.run(
|
|
197
|
+
list(_NPX_UPDATE_CMD),
|
|
198
|
+
check=False,
|
|
199
|
+
timeout=_NPX_TIMEOUT_SECONDS,
|
|
200
|
+
env=env,
|
|
201
|
+
)
|
|
202
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
203
|
+
sys.stderr.write(
|
|
204
|
+
f"[arkaos] npx arkaos@latest update failed: {exc}\n"
|
|
205
|
+
"[arkaos] continuing with the sync engine using the "
|
|
206
|
+
"currently-installed core.\n"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def main(argv: list[str]) -> int:
|
|
211
|
+
"""CLI entry: python -m core.sync.update_orchestrator --home X --skills Y."""
|
|
212
|
+
import argparse
|
|
213
|
+
parser = argparse.ArgumentParser(description="ArkaOS one-stop /arka update")
|
|
214
|
+
parser.add_argument("--home", required=True)
|
|
215
|
+
parser.add_argument("--skills", required=True)
|
|
216
|
+
parser.add_argument("--output", choices=["text", "json"], default="text")
|
|
217
|
+
args = parser.parse_args(argv[1:])
|
|
218
|
+
|
|
219
|
+
installed, latest, report = orchestrate(
|
|
220
|
+
arkaos_home=Path(args.home),
|
|
221
|
+
skills_dir=Path(args.skills),
|
|
222
|
+
home_path=str(Path.home()),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if args.output == "json":
|
|
226
|
+
payload = {
|
|
227
|
+
"installed_version_before": installed,
|
|
228
|
+
"latest_version_seen": latest,
|
|
229
|
+
"report": report.model_dump(),
|
|
230
|
+
}
|
|
231
|
+
print(json.dumps(payload, indent=2))
|
|
232
|
+
else:
|
|
233
|
+
from core.sync.reporter import format_report
|
|
234
|
+
print(f"Installed: {installed or 'unknown'}")
|
|
235
|
+
print(f"Latest published: {latest or 'unknown'}")
|
|
236
|
+
if installed and latest and _is_older(installed, latest):
|
|
237
|
+
print(f"Updated from {installed} → {latest} via npx arkaos@latest")
|
|
238
|
+
print()
|
|
239
|
+
print(format_report(report))
|
|
240
|
+
return 0
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
if __name__ == "__main__":
|
|
244
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Persona } from '~/types'
|
|
3
|
+
|
|
4
|
+
// PR62 v2.79.0 — 4-step AI-powered persona wizard.
|
|
5
|
+
// Replaces the original "fill a 30-field form" UX with:
|
|
6
|
+
// 1. Sources → operator pastes URLs / picks files, optionally types a name
|
|
7
|
+
// 2. Ingest → existing /api/knowledge/ingest-bulk fans the URLs out into
|
|
8
|
+
// background jobs and shows progress via WebSocket
|
|
9
|
+
// 3. Build → /api/personas/build (PR57) reads the indexed chunks and
|
|
10
|
+
// returns a draft Persona; operator reviews + edits
|
|
11
|
+
// 4. Save → POST /api/personas (manual create path), optional
|
|
12
|
+
// clone-to-agent at the end
|
|
13
|
+
//
|
|
14
|
+
// The wizard NEVER auto-saves — every transition is operator-confirmed.
|
|
15
|
+
|
|
16
|
+
const { apiBase } = useApi()
|
|
17
|
+
const toast = useToast()
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
(e: 'completed', persona: Persona): void
|
|
21
|
+
(e: 'cancelled'): void
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
type Step = 1 | 2 | 3 | 4
|
|
25
|
+
const step = ref<Step>(1)
|
|
26
|
+
|
|
27
|
+
// ─── Step 1 state ────────────────────────────────────────────────────────
|
|
28
|
+
const name = ref('')
|
|
29
|
+
const sourceLabel = ref('')
|
|
30
|
+
const sources = ref('')
|
|
31
|
+
const skipIngest = ref(false)
|
|
32
|
+
const sourceLineCount = computed(() =>
|
|
33
|
+
sources.value
|
|
34
|
+
.split('\n')
|
|
35
|
+
.map((s) => s.trim())
|
|
36
|
+
.filter((s) => s.length > 0)
|
|
37
|
+
.length,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// ─── Step 2 state ────────────────────────────────────────────────────────
|
|
41
|
+
const ingestJobs = ref<Array<{
|
|
42
|
+
source: string
|
|
43
|
+
job_id?: string
|
|
44
|
+
status: 'queued' | 'processing' | 'completed' | 'failed'
|
|
45
|
+
progress: number
|
|
46
|
+
error?: string
|
|
47
|
+
}>>([])
|
|
48
|
+
let ws: WebSocket | null = null
|
|
49
|
+
const allIngestComplete = computed(() =>
|
|
50
|
+
ingestJobs.value.length > 0
|
|
51
|
+
&& ingestJobs.value.every((j) => j.status === 'completed' || j.status === 'failed'),
|
|
52
|
+
)
|
|
53
|
+
const ingestCompletedCount = computed(() =>
|
|
54
|
+
ingestJobs.value.filter((j) => j.status === 'completed').length,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
// ─── Step 3 state ────────────────────────────────────────────────────────
|
|
58
|
+
const draft = ref<Persona | null>(null)
|
|
59
|
+
const building = ref(false)
|
|
60
|
+
const buildError = ref<string | null>(null)
|
|
61
|
+
const chunksUsed = ref<number | null>(null)
|
|
62
|
+
|
|
63
|
+
// ─── Step 4 state ────────────────────────────────────────────────────────
|
|
64
|
+
const saving = ref(false)
|
|
65
|
+
const saveAndClone = ref(false)
|
|
66
|
+
const cloneDept = ref('strategy')
|
|
67
|
+
const cloneTier = ref<'1' | '2' | '3'>('2')
|
|
68
|
+
|
|
69
|
+
const departmentOptions = [
|
|
70
|
+
'dev', 'marketing', 'brand', 'finance', 'strategy', 'ecom', 'kb', 'ops',
|
|
71
|
+
'pm', 'saas', 'landing', 'content', 'community', 'sales', 'leadership', 'org',
|
|
72
|
+
].map((d) => ({ label: d, value: d }))
|
|
73
|
+
|
|
74
|
+
const tierOptions = [
|
|
75
|
+
{ label: 'Tier 1 — Squad Lead', value: '1' },
|
|
76
|
+
{ label: 'Tier 2 — Specialist', value: '2' },
|
|
77
|
+
{ label: 'Tier 3 — Support', value: '3' },
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
// ─── Step 1 → 2 transition ───────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async function startIngest() {
|
|
84
|
+
if (skipIngest.value) {
|
|
85
|
+
// Jump straight to step 3 — operator says content is already indexed.
|
|
86
|
+
step.value = 3
|
|
87
|
+
await runBuild()
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
const cleaned = sources.value
|
|
91
|
+
.split('\n')
|
|
92
|
+
.map((s) => s.trim())
|
|
93
|
+
.filter((s) => s.length > 0)
|
|
94
|
+
if (cleaned.length === 0 || !name.value.trim()) return
|
|
95
|
+
step.value = 2
|
|
96
|
+
ingestJobs.value = cleaned.map((source) => ({
|
|
97
|
+
source,
|
|
98
|
+
status: 'queued',
|
|
99
|
+
progress: 0,
|
|
100
|
+
}))
|
|
101
|
+
try {
|
|
102
|
+
const res = await $fetch<{ jobs: Array<{ source: string, job_id?: string, error?: string }>, count: number }>(
|
|
103
|
+
`${apiBase}/api/knowledge/ingest-bulk`,
|
|
104
|
+
{ method: 'POST', body: { sources: cleaned } },
|
|
105
|
+
)
|
|
106
|
+
res.jobs.forEach((j) => {
|
|
107
|
+
const row = ingestJobs.value.find((r) => r.source === j.source)
|
|
108
|
+
if (!row) return
|
|
109
|
+
if (j.error) {
|
|
110
|
+
row.status = 'failed'
|
|
111
|
+
row.error = j.error
|
|
112
|
+
} else if (j.job_id) {
|
|
113
|
+
row.job_id = j.job_id
|
|
114
|
+
row.status = 'processing'
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
connectWebSocket()
|
|
118
|
+
} catch (err) {
|
|
119
|
+
toast.add({
|
|
120
|
+
title: 'Ingest failed',
|
|
121
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
122
|
+
color: 'error',
|
|
123
|
+
})
|
|
124
|
+
step.value = 1
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
function connectWebSocket() {
|
|
130
|
+
if (ws && ws.readyState === WebSocket.OPEN) return
|
|
131
|
+
const wsUrl = apiBase.replace('http://', 'ws://').replace('https://', 'wss://') + '/ws/tasks'
|
|
132
|
+
ws = new WebSocket(wsUrl)
|
|
133
|
+
ws.onmessage = (event) => {
|
|
134
|
+
try {
|
|
135
|
+
const data = JSON.parse(event.data)
|
|
136
|
+
const row = ingestJobs.value.find((j) => j.job_id === data.job_id)
|
|
137
|
+
if (!row) return
|
|
138
|
+
if (data.type === 'job_progress') {
|
|
139
|
+
row.progress = data.progress
|
|
140
|
+
row.status = 'processing'
|
|
141
|
+
} else if (data.type === 'job_complete') {
|
|
142
|
+
row.status = 'completed'
|
|
143
|
+
row.progress = 100
|
|
144
|
+
} else if (data.type === 'job_failed') {
|
|
145
|
+
row.status = 'failed'
|
|
146
|
+
row.error = data.error
|
|
147
|
+
}
|
|
148
|
+
} catch { /* ignore malformed messages */ }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
function disconnectWebSocket() {
|
|
154
|
+
if (ws) {
|
|
155
|
+
try { ws.close() } catch { /* already closed */ }
|
|
156
|
+
ws = null
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
onBeforeUnmount(() => {
|
|
162
|
+
disconnectWebSocket()
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
// ─── Step 3: build the persona draft ────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async function runBuild() {
|
|
170
|
+
building.value = true
|
|
171
|
+
buildError.value = null
|
|
172
|
+
draft.value = null
|
|
173
|
+
try {
|
|
174
|
+
const res = await $fetch<{ persona: Persona, chunks_used: number, provider_name: string }>(
|
|
175
|
+
`${apiBase}/api/personas/build`,
|
|
176
|
+
{
|
|
177
|
+
method: 'POST',
|
|
178
|
+
body: {
|
|
179
|
+
name: name.value.trim(),
|
|
180
|
+
source_label: sourceLabel.value.trim() || name.value.trim(),
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
if ('error' in res && typeof (res as any).error === 'string') {
|
|
185
|
+
throw new Error((res as any).error)
|
|
186
|
+
}
|
|
187
|
+
draft.value = res.persona
|
|
188
|
+
chunksUsed.value = res.chunks_used
|
|
189
|
+
step.value = 4
|
|
190
|
+
} catch (err) {
|
|
191
|
+
buildError.value = err instanceof Error ? err.message : 'unknown error'
|
|
192
|
+
} finally {
|
|
193
|
+
building.value = false
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
// ─── Step 4: save + optional clone ──────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
async function savePersona() {
|
|
202
|
+
if (!draft.value) return
|
|
203
|
+
saving.value = true
|
|
204
|
+
try {
|
|
205
|
+
const created = await $fetch<Persona>(`${apiBase}/api/personas`, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
body: draft.value,
|
|
208
|
+
})
|
|
209
|
+
if (saveAndClone.value && cloneDept.value && cloneTier.value) {
|
|
210
|
+
await $fetch(`${apiBase}/api/personas/${created.id}/clone`, {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
body: { department: cloneDept.value, tier: Number(cloneTier.value) },
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
toast.add({
|
|
216
|
+
title: saveAndClone.value ? 'Persona saved + agent cloned' : 'Persona saved',
|
|
217
|
+
description: `${created.name} is now in your board.`,
|
|
218
|
+
color: 'success',
|
|
219
|
+
})
|
|
220
|
+
emit('completed', created)
|
|
221
|
+
} catch (err) {
|
|
222
|
+
toast.add({
|
|
223
|
+
title: 'Save failed',
|
|
224
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
225
|
+
color: 'error',
|
|
226
|
+
})
|
|
227
|
+
} finally {
|
|
228
|
+
saving.value = false
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
// ─── Auto-advance when ingest completes ─────────────────────────────────
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
watch(allIngestComplete, async (done) => {
|
|
237
|
+
if (done && step.value === 2 && ingestCompletedCount.value > 0) {
|
|
238
|
+
step.value = 3
|
|
239
|
+
await runBuild()
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
function cancel() {
|
|
245
|
+
disconnectWebSocket()
|
|
246
|
+
emit('cancelled')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
function backToStep1() {
|
|
251
|
+
disconnectWebSocket()
|
|
252
|
+
step.value = 1
|
|
253
|
+
ingestJobs.value = []
|
|
254
|
+
}
|
|
255
|
+
</script>
|
|
256
|
+
|
|
257
|
+
<template>
|
|
258
|
+
<UCard>
|
|
259
|
+
<template #header>
|
|
260
|
+
<div class="flex items-center justify-between">
|
|
261
|
+
<div>
|
|
262
|
+
<h3 class="text-lg font-semibold">AI Persona Builder</h3>
|
|
263
|
+
<p class="text-sm text-muted mt-1">
|
|
264
|
+
Step {{ step }} of 4 — {{ {
|
|
265
|
+
1: 'Sources',
|
|
266
|
+
2: 'Indexing',
|
|
267
|
+
3: 'Generating DNA',
|
|
268
|
+
4: 'Review & save',
|
|
269
|
+
}[step] }}
|
|
270
|
+
</p>
|
|
271
|
+
</div>
|
|
272
|
+
<UButton
|
|
273
|
+
label="Cancel"
|
|
274
|
+
variant="ghost"
|
|
275
|
+
color="neutral"
|
|
276
|
+
size="sm"
|
|
277
|
+
@click="cancel"
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
</template>
|
|
281
|
+
|
|
282
|
+
<!-- Progress indicator -->
|
|
283
|
+
<div class="flex items-center gap-2 mb-6">
|
|
284
|
+
<div
|
|
285
|
+
v-for="s in [1, 2, 3, 4] as Step[]"
|
|
286
|
+
:key="s"
|
|
287
|
+
class="flex-1 h-1 rounded-full"
|
|
288
|
+
:class="s <= step ? 'bg-primary' : 'bg-muted/30'"
|
|
289
|
+
/>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<!-- Step 1: Sources -->
|
|
293
|
+
<div v-if="step === 1" class="space-y-4">
|
|
294
|
+
<UFormField label="Person name" required>
|
|
295
|
+
<UInput
|
|
296
|
+
v-model="name"
|
|
297
|
+
placeholder="e.g. Alex Hormozi"
|
|
298
|
+
size="lg"
|
|
299
|
+
class="w-full"
|
|
300
|
+
/>
|
|
301
|
+
</UFormField>
|
|
302
|
+
|
|
303
|
+
<UFormField label="Source label (optional)" help="How this person should appear in the persona's source field. Defaults to the name above.">
|
|
304
|
+
<UInput
|
|
305
|
+
v-model="sourceLabel"
|
|
306
|
+
placeholder="e.g. Alex Hormozi — $100M Offers / $100M Leads"
|
|
307
|
+
class="w-full"
|
|
308
|
+
/>
|
|
309
|
+
</UFormField>
|
|
310
|
+
|
|
311
|
+
<UFormField
|
|
312
|
+
label="Sources (one URL per line)"
|
|
313
|
+
help="YouTube videos, articles, PDFs, blog posts about this person. The builder will search the indexed chunks and synthesise their behavioural DNA. Up to 50 sources per batch."
|
|
314
|
+
>
|
|
315
|
+
<UTextarea
|
|
316
|
+
v-model="sources"
|
|
317
|
+
:rows="6"
|
|
318
|
+
placeholder="https://www.youtube.com/watch?v=... https://example.com/article https://example.com/paper.pdf"
|
|
319
|
+
class="w-full font-mono text-sm"
|
|
320
|
+
:disabled="skipIngest"
|
|
321
|
+
/>
|
|
322
|
+
</UFormField>
|
|
323
|
+
|
|
324
|
+
<div class="flex items-center justify-between text-xs text-muted">
|
|
325
|
+
<span>{{ sourceLineCount }} source{{ sourceLineCount === 1 ? '' : 's' }} detected</span>
|
|
326
|
+
<UCheckbox
|
|
327
|
+
v-model="skipIngest"
|
|
328
|
+
label="Skip ingest — content for this person is already in the knowledge base"
|
|
329
|
+
/>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<div class="flex justify-end gap-2 pt-4">
|
|
333
|
+
<UButton
|
|
334
|
+
:label="skipIngest ? 'Generate from existing knowledge' : `Index ${sourceLineCount} source${sourceLineCount === 1 ? '' : 's'} & build`"
|
|
335
|
+
icon="i-lucide-arrow-right"
|
|
336
|
+
:disabled="!name.trim() || (!skipIngest && sourceLineCount === 0) || sourceLineCount > 50"
|
|
337
|
+
size="md"
|
|
338
|
+
@click="startIngest"
|
|
339
|
+
/>
|
|
340
|
+
</div>
|
|
341
|
+
<p v-if="sourceLineCount > 50" class="text-xs text-red-400">
|
|
342
|
+
Over the 50-source cap. Trim the list before continuing.
|
|
343
|
+
</p>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
<!-- Step 2: Ingest progress -->
|
|
347
|
+
<div v-else-if="step === 2" class="space-y-4">
|
|
348
|
+
<p class="text-sm text-muted">
|
|
349
|
+
Indexing {{ ingestJobs.length }} source{{ ingestJobs.length === 1 ? '' : 's' }} into the knowledge base.
|
|
350
|
+
This auto-advances when complete.
|
|
351
|
+
</p>
|
|
352
|
+
<div class="space-y-2">
|
|
353
|
+
<div
|
|
354
|
+
v-for="(job, idx) in ingestJobs"
|
|
355
|
+
:key="idx"
|
|
356
|
+
class="rounded-lg border border-default p-3"
|
|
357
|
+
>
|
|
358
|
+
<div class="flex items-center gap-3">
|
|
359
|
+
<UIcon
|
|
360
|
+
:name="{
|
|
361
|
+
queued: 'i-lucide-clock',
|
|
362
|
+
processing: 'i-lucide-loader-2 animate-spin',
|
|
363
|
+
completed: 'i-lucide-check-circle',
|
|
364
|
+
failed: 'i-lucide-x-circle',
|
|
365
|
+
}[job.status]"
|
|
366
|
+
:class="{
|
|
367
|
+
queued: 'text-muted',
|
|
368
|
+
processing: 'text-primary',
|
|
369
|
+
completed: 'text-green-500',
|
|
370
|
+
failed: 'text-red-500',
|
|
371
|
+
}[job.status]"
|
|
372
|
+
class="size-4 shrink-0"
|
|
373
|
+
/>
|
|
374
|
+
<div class="flex-1 min-w-0">
|
|
375
|
+
<p class="text-sm font-mono truncate">{{ job.source }}</p>
|
|
376
|
+
<UProgress
|
|
377
|
+
v-if="job.status === 'processing' || job.status === 'queued'"
|
|
378
|
+
:value="job.progress"
|
|
379
|
+
:max="100"
|
|
380
|
+
size="xs"
|
|
381
|
+
class="mt-1"
|
|
382
|
+
/>
|
|
383
|
+
<p v-if="job.error" class="text-xs text-red-400 mt-1">{{ job.error }}</p>
|
|
384
|
+
</div>
|
|
385
|
+
<span class="text-xs text-muted">{{ job.progress }}%</span>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
<!-- Step 3: Building -->
|
|
392
|
+
<div v-else-if="step === 3" class="space-y-4">
|
|
393
|
+
<div v-if="building" class="flex flex-col items-center gap-4 py-12">
|
|
394
|
+
<UIcon name="i-lucide-loader-2" class="size-12 animate-spin text-primary" />
|
|
395
|
+
<p class="text-sm text-muted">
|
|
396
|
+
Reading indexed chunks about <strong>{{ name }}</strong>…
|
|
397
|
+
</p>
|
|
398
|
+
<p class="text-xs text-muted">
|
|
399
|
+
The builder searches the vector store, joins the top chunks, and asks the configured LLM to extract a behavioural-DNA persona.
|
|
400
|
+
</p>
|
|
401
|
+
</div>
|
|
402
|
+
<div v-else-if="buildError" class="rounded-lg border border-red-500/20 bg-red-500/5 p-4">
|
|
403
|
+
<div class="flex items-start gap-3">
|
|
404
|
+
<UIcon name="i-lucide-alert-circle" class="size-5 text-red-500 mt-0.5 shrink-0" />
|
|
405
|
+
<div class="flex-1">
|
|
406
|
+
<p class="text-sm font-medium text-red-400">Build failed</p>
|
|
407
|
+
<p class="text-xs text-muted mt-1">{{ buildError }}</p>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
<div class="flex gap-2 mt-3">
|
|
411
|
+
<UButton label="Retry" variant="outline" size="sm" @click="runBuild" />
|
|
412
|
+
<UButton label="Back to sources" variant="ghost" size="sm" @click="backToStep1" />
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
<!-- Step 4: Review & save -->
|
|
418
|
+
<div v-else-if="step === 4 && draft" class="space-y-4">
|
|
419
|
+
<div class="rounded-lg border border-green-500/20 bg-green-500/5 p-3">
|
|
420
|
+
<p class="text-sm text-green-400">
|
|
421
|
+
<UIcon name="i-lucide-sparkles" class="size-4 inline" />
|
|
422
|
+
Built from <strong>{{ chunksUsed }}</strong> knowledge chunk{{ chunksUsed === 1 ? '' : 's' }}. Edit any field below before saving.
|
|
423
|
+
</p>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<fieldset class="space-y-3">
|
|
427
|
+
<legend class="text-xs font-bold uppercase tracking-widest text-muted mb-2">Identity</legend>
|
|
428
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
429
|
+
<UFormField label="Name">
|
|
430
|
+
<UInput v-model="draft.name" class="w-full" />
|
|
431
|
+
</UFormField>
|
|
432
|
+
<UFormField label="Title">
|
|
433
|
+
<UInput v-model="draft.title" class="w-full" />
|
|
434
|
+
</UFormField>
|
|
435
|
+
</div>
|
|
436
|
+
<UFormField label="Tagline">
|
|
437
|
+
<UInput v-model="draft.tagline" class="w-full" />
|
|
438
|
+
</UFormField>
|
|
439
|
+
</fieldset>
|
|
440
|
+
|
|
441
|
+
<fieldset class="space-y-3">
|
|
442
|
+
<legend class="text-xs font-bold uppercase tracking-widest text-muted mb-2">Behavioural DNA</legend>
|
|
443
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
444
|
+
<UFormField label="MBTI">
|
|
445
|
+
<UInput v-model="draft.mbti" class="w-full" />
|
|
446
|
+
</UFormField>
|
|
447
|
+
<UFormField label="DISC primary">
|
|
448
|
+
<UInput v-model="draft.disc.primary" class="w-full" />
|
|
449
|
+
</UFormField>
|
|
450
|
+
<UFormField label="Enneagram type">
|
|
451
|
+
<UInput v-model.number="draft.enneagram.type" type="number" :min="1" :max="9" class="w-full" />
|
|
452
|
+
</UFormField>
|
|
453
|
+
<UFormField label="Enneagram wing">
|
|
454
|
+
<UInput v-model.number="draft.enneagram.wing" type="number" :min="1" :max="9" class="w-full" />
|
|
455
|
+
</UFormField>
|
|
456
|
+
</div>
|
|
457
|
+
</fieldset>
|
|
458
|
+
|
|
459
|
+
<fieldset class="space-y-3">
|
|
460
|
+
<legend class="text-xs font-bold uppercase tracking-widest text-muted mb-2">Knowledge</legend>
|
|
461
|
+
<UFormField label="Mental models" help="comma-separated">
|
|
462
|
+
<UInput
|
|
463
|
+
:model-value="draft.mental_models.join(', ')"
|
|
464
|
+
@update:model-value="(v: string) => draft && (draft.mental_models = v.split(',').map(s => s.trim()).filter(Boolean))"
|
|
465
|
+
class="w-full"
|
|
466
|
+
/>
|
|
467
|
+
</UFormField>
|
|
468
|
+
<UFormField label="Expertise domains" help="comma-separated">
|
|
469
|
+
<UInput
|
|
470
|
+
:model-value="draft.expertise_domains.join(', ')"
|
|
471
|
+
@update:model-value="(v: string) => draft && (draft.expertise_domains = v.split(',').map(s => s.trim()).filter(Boolean))"
|
|
472
|
+
class="w-full"
|
|
473
|
+
/>
|
|
474
|
+
</UFormField>
|
|
475
|
+
</fieldset>
|
|
476
|
+
|
|
477
|
+
<fieldset class="space-y-3">
|
|
478
|
+
<legend class="text-xs font-bold uppercase tracking-widest text-muted mb-2">Save options</legend>
|
|
479
|
+
<UCheckbox
|
|
480
|
+
v-model="saveAndClone"
|
|
481
|
+
label="Also clone to an agent immediately"
|
|
482
|
+
/>
|
|
483
|
+
<div v-if="saveAndClone" class="grid grid-cols-2 gap-3 pl-6">
|
|
484
|
+
<UFormField label="Department">
|
|
485
|
+
<USelect v-model="cloneDept" :items="departmentOptions" class="w-full" />
|
|
486
|
+
</UFormField>
|
|
487
|
+
<UFormField label="Tier">
|
|
488
|
+
<USelect v-model="cloneTier" :items="tierOptions" class="w-full" />
|
|
489
|
+
</UFormField>
|
|
490
|
+
</div>
|
|
491
|
+
</fieldset>
|
|
492
|
+
|
|
493
|
+
<div class="flex justify-end gap-2 pt-4">
|
|
494
|
+
<UButton label="Back" variant="ghost" @click="backToStep1" />
|
|
495
|
+
<UButton
|
|
496
|
+
:label="saveAndClone ? 'Save & clone' : 'Save persona'"
|
|
497
|
+
icon="i-lucide-check"
|
|
498
|
+
:loading="saving"
|
|
499
|
+
@click="savePersona"
|
|
500
|
+
/>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
</UCard>
|
|
504
|
+
</template>
|
|
@@ -9,8 +9,30 @@ const { data, status, error, refresh } = fetchApi<{ personas: Persona[]; total:
|
|
|
9
9
|
|
|
10
10
|
const personas = computed(() => data.value?.personas ?? [])
|
|
11
11
|
|
|
12
|
-
// ---
|
|
13
|
-
|
|
12
|
+
// --- Creation mode ---
|
|
13
|
+
// PR62 v2.79.0 — three modes: list (default), wizard (AI builder), manual.
|
|
14
|
+
// The wizard is the new primary path; manual stays as fallback for
|
|
15
|
+
// operators who want to type every DNA field by hand.
|
|
16
|
+
type CreateMode = 'list' | 'wizard' | 'manual'
|
|
17
|
+
const createMode = ref<CreateMode>('list')
|
|
18
|
+
const showForm = computed(() => createMode.value === 'manual')
|
|
19
|
+
|
|
20
|
+
function startWizard() {
|
|
21
|
+
createMode.value = 'wizard'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function startManual() {
|
|
25
|
+
createMode.value = 'manual'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cancelCreation() {
|
|
29
|
+
createMode.value = 'list'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function onWizardComplete() {
|
|
33
|
+
createMode.value = 'list'
|
|
34
|
+
await refresh()
|
|
35
|
+
}
|
|
14
36
|
|
|
15
37
|
// --- Form state ---
|
|
16
38
|
function defaultForm() {
|
|
@@ -216,11 +238,29 @@ function discColor(disc: string): string {
|
|
|
216
238
|
|
|
217
239
|
<template #right>
|
|
218
240
|
<UButton
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
241
|
+
v-if="createMode === 'list'"
|
|
242
|
+
label="AI Builder"
|
|
243
|
+
icon="i-lucide-sparkles"
|
|
244
|
+
color="primary"
|
|
245
|
+
size="sm"
|
|
246
|
+
@click="startWizard"
|
|
247
|
+
/>
|
|
248
|
+
<UButton
|
|
249
|
+
v-if="createMode === 'list'"
|
|
250
|
+
label="Manual"
|
|
251
|
+
icon="i-lucide-plus"
|
|
252
|
+
variant="outline"
|
|
253
|
+
size="sm"
|
|
254
|
+
class="ml-2"
|
|
255
|
+
@click="startManual"
|
|
256
|
+
/>
|
|
257
|
+
<UButton
|
|
258
|
+
v-else
|
|
259
|
+
label="Back to list"
|
|
260
|
+
icon="i-lucide-arrow-left"
|
|
261
|
+
variant="ghost"
|
|
222
262
|
size="sm"
|
|
223
|
-
@click="
|
|
263
|
+
@click="cancelCreation"
|
|
224
264
|
/>
|
|
225
265
|
</template>
|
|
226
266
|
</UDashboardNavbar>
|
|
@@ -242,7 +282,15 @@ function discColor(disc: string): string {
|
|
|
242
282
|
|
|
243
283
|
<!-- Content -->
|
|
244
284
|
<template v-else>
|
|
245
|
-
<!--
|
|
285
|
+
<!-- PR62: AI Persona Wizard -->
|
|
286
|
+
<PersonaWizard
|
|
287
|
+
v-if="createMode === 'wizard'"
|
|
288
|
+
class="mb-8"
|
|
289
|
+
@completed="onWizardComplete"
|
|
290
|
+
@cancelled="cancelCreation"
|
|
291
|
+
/>
|
|
292
|
+
|
|
293
|
+
<!-- Manual create form (legacy / fallback) -->
|
|
246
294
|
<UCard v-if="showForm" class="mb-8">
|
|
247
295
|
<form @submit.prevent="createPersona" class="space-y-8 p-2">
|
|
248
296
|
<!-- Identity -->
|
|
@@ -21,15 +21,31 @@ Hybrid sync engine: Python handles deterministic operations (MCPs, settings, des
|
|
|
21
21
|
|
|
22
22
|
## Orchestration (Summary)
|
|
23
23
|
|
|
24
|
-
1. **
|
|
24
|
+
1. **One-stop: npm refresh + engine (PR61 v2.78.0 orchestrator).**
|
|
25
25
|
```bash
|
|
26
|
-
cd $ARKAOS_ROOT && python -m core.sync.
|
|
26
|
+
cd $ARKAOS_ROOT && python -m core.sync.update_orchestrator --home ~/.arkaos --skills ~/.claude/skills --output json
|
|
27
27
|
```
|
|
28
|
-
|
|
28
|
+
The orchestrator detects whether the running ArkaOS is behind npm
|
|
29
|
+
latest. When stale, it shells out to `npx arkaos@latest update`
|
|
30
|
+
first so the sync engine below reads fresh code; when current, it
|
|
31
|
+
skips straight to the engine. Either way it runs the
|
|
32
|
+
deterministic engine (manifest, discovery, MCP sync, settings
|
|
33
|
+
sync, descriptors, content, agents) and writes
|
|
34
|
+
`~/.arkaos/sync-state.json`.
|
|
35
|
+
|
|
36
|
+
Probe is cached for 1 hour in `~/.arkaos/npm-latest.cache.json`
|
|
37
|
+
to keep repeat runs cheap. Offline / `npx` missing → orchestrator
|
|
38
|
+
silently skips the npm step and falls through to the engine.
|
|
39
|
+
|
|
40
|
+
Fallback (no orchestrator): the underlying engine still runs the
|
|
41
|
+
same way via `python -m core.sync.engine ...` for callers that
|
|
42
|
+
don't need the version-drift gate.
|
|
29
43
|
|
|
30
44
|
2. **Phase 4 (intelligent, AI subagent):** After the engine completes, dispatch ONE subagent with the engine's JSON report + the feature registry (`core/sync/features/*.yaml`). The subagent injects/removes feature sections in each `~/.claude/skills/arka-{ecosystem}/SKILL.md` while preserving all custom content.
|
|
31
45
|
|
|
32
|
-
3. **Report:** Display the formatted summary returned by the engine
|
|
46
|
+
3. **Report:** Display the formatted summary returned by the engine,
|
|
47
|
+
plus `installed_version_before` / `latest_version_seen` from the
|
|
48
|
+
orchestrator so the operator sees what got refreshed.
|
|
33
49
|
|
|
34
50
|
## Error Handling (Summary)
|
|
35
51
|
|
package/package.json
CHANGED