arkaos 4.3.6 → 4.5.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/bin/arka-py +22 -7
- package/config/hooks/_lib/arka_python.ps1 +31 -0
- package/config/hooks/_lib/arka_python.sh +38 -0
- package/config/hooks/post-tool-use.ps1 +8 -0
- package/config/hooks/post-tool-use.sh +11 -5
- package/config/hooks/pre-tool-use.ps1 +11 -6
- package/config/hooks/pre-tool-use.sh +11 -6
- package/config/hooks/session-start.ps1 +10 -0
- package/config/hooks/session-start.sh +5 -0
- package/config/hooks/stop.ps1 +9 -1
- package/config/hooks/stop.sh +11 -5
- package/config/hooks/user-prompt-submit.ps1 +18 -0
- package/config/hooks/user-prompt-submit.sh +11 -5
- package/config/mcp-policy.yaml +8 -8
- package/core/agents/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/agents/__pycache__/loader.cpython-312.pyc +0 -0
- package/core/agents/__pycache__/registry_gen.cpython-313.pyc +0 -0
- package/core/agents/__pycache__/schema.cpython-312.pyc +0 -0
- package/core/agents/registry_gen.py +3 -2
- package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
- package/core/cognition/scheduler/daemon.py +7 -3
- package/core/forge/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/core/forge/orchestrator.py +2 -1
- package/core/hooks/__pycache__/_shared.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/_shared.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/core/hooks/_shared.py +29 -9
- package/core/hooks/stop.py +4 -3
- package/core/hooks/user_prompt_submit.py +5 -4
- package/core/knowledge/__pycache__/indexer.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/core/knowledge/indexer.py +1 -1
- package/core/knowledge/vector_store.py +10 -3
- package/core/runtime/__pycache__/model_routing_check.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/model_routing_context.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/native_usage.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/native_usage.cpython-314.pyc +0 -0
- package/core/runtime/gateway/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/runtime/gateway/__pycache__/__main__.cpython-313.pyc +0 -0
- package/core/runtime/gateway/__pycache__/litellm_config.cpython-313.pyc +0 -0
- package/core/runtime/native_usage.py +2 -1
- package/core/shared/__pycache__/temp_paths.cpython-313.pyc +0 -0
- package/core/shared/__pycache__/temp_paths.cpython-314.pyc +0 -0
- package/core/shared/temp_paths.py +40 -0
- package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/kb_cache.cpython-314.pyc +0 -0
- package/core/synapse/kb_cache.py +3 -2
- package/core/sync/__pycache__/agent_provisioner.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/ai_mcp_decider.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/content_syncer.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/descriptor_syncer.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/discovery.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/manifest.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/mcp_optimizer.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/mcp_syncer.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/policy_loader.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/reporter.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/settings_syncer.cpython-313.pyc +0 -0
- package/core/sync/agent_provisioner.py +5 -5
- package/core/sync/ai_mcp_decider.py +2 -2
- package/core/sync/content_syncer.py +14 -14
- package/core/sync/descriptor_syncer.py +2 -2
- package/core/sync/discovery.py +4 -4
- package/core/sync/engine.py +4 -4
- package/core/sync/manifest.py +1 -1
- package/core/sync/mcp_optimizer.py +5 -5
- package/core/sync/mcp_syncer.py +7 -3
- package/core/sync/policy_loader.py +1 -1
- package/core/sync/reporter.py +1 -1
- package/core/sync/settings_syncer.py +2 -2
- package/core/terminal/__pycache__/session.cpython-313.pyc +0 -0
- package/core/terminal/__pycache__/session_windows.cpython-313.pyc +0 -0
- package/core/terminal/session.py +66 -11
- package/core/terminal/session_windows.py +211 -0
- package/core/workflow/__pycache__/flow_authorization.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/flow_authorization.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/marker_cache.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/marker_cache.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/research_gate.cpython-314.pyc +0 -0
- package/core/workflow/flow_authorization.py +2 -1
- package/core/workflow/flow_enforcer.py +2 -1
- package/core/workflow/marker_cache.py +2 -1
- package/core/workflow/research_gate.py +2 -1
- package/dashboard/app/pages/index.vue +6 -1
- package/departments/dev/skills/mcp/SKILL.md +3 -1
- package/departments/ops/skills/update/references/sync-engine.md +2 -1
- package/departments/quality/agents/copy-director.yaml +2 -0
- package/departments/quality/agents/tech-director.yaml +2 -0
- package/installer/cli.js +5 -4
- package/installer/core-snapshot.js +53 -0
- package/installer/doctor.js +8 -3
- package/installer/index.js +29 -6
- package/installer/migrate.js +7 -4
- package/installer/update.js +13 -0
- package/knowledge/agents-registry-v2.json +487 -120
- package/package.json +1 -1
- package/pyproject.toml +2 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/__pycache__/synapse-bridge.cpython-313.pyc +0 -0
- package/scripts/dashboard-api.py +69 -33
- package/scripts/knowledge-index.py +35 -2
- package/scripts/start-dashboard.ps1 +34 -14
- package/scripts/synapse-bridge.py +3 -1
package/core/terminal/session.py
CHANGED
|
@@ -21,16 +21,23 @@ Design notes
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
23
|
import errno
|
|
24
|
-
import fcntl
|
|
25
24
|
import os
|
|
26
|
-
import pty
|
|
27
25
|
import secrets
|
|
26
|
+
import shutil
|
|
28
27
|
import signal
|
|
29
28
|
import struct
|
|
30
|
-
import termios
|
|
31
29
|
import time
|
|
32
30
|
from typing import Any, Optional
|
|
33
31
|
|
|
32
|
+
try: # POSIX-only: the Windows backend lives in session_windows.py
|
|
33
|
+
import fcntl
|
|
34
|
+
import pty
|
|
35
|
+
import termios
|
|
36
|
+
_PTY_SUPPORTED = True
|
|
37
|
+
except ModuleNotFoundError:
|
|
38
|
+
fcntl = pty = termios = None # type: ignore[assignment]
|
|
39
|
+
_PTY_SUPPORTED = False
|
|
40
|
+
|
|
34
41
|
from core.terminal import audit
|
|
35
42
|
|
|
36
43
|
|
|
@@ -39,10 +46,47 @@ class SessionCapacityError(RuntimeError):
|
|
|
39
46
|
|
|
40
47
|
|
|
41
48
|
def _default_shell() -> str:
|
|
49
|
+
if os.name == "nt":
|
|
50
|
+
# Windows PowerShell renders reliably under ConPTY; bare cmd.exe can
|
|
51
|
+
# emit no prompt when the API is launched without an attached
|
|
52
|
+
# console, and the WindowsApps "pwsh" alias is a Store reparse point
|
|
53
|
+
# that misbehaves when spawned programmatically — so resolve the real
|
|
54
|
+
# System32 powershell.exe first.
|
|
55
|
+
return _resolve_windows_shell()
|
|
42
56
|
return os.environ.get("SHELL") or "/bin/zsh"
|
|
43
57
|
|
|
44
58
|
|
|
59
|
+
def _resolve_windows_shell() -> str:
|
|
60
|
+
"""Pick a Windows shell that works under ConPTY, skipping Store aliases."""
|
|
61
|
+
candidates = []
|
|
62
|
+
pwsh = shutil.which("pwsh")
|
|
63
|
+
if pwsh and "windowsapps" not in pwsh.lower():
|
|
64
|
+
candidates.append(pwsh)
|
|
65
|
+
powershell = shutil.which("powershell")
|
|
66
|
+
if powershell:
|
|
67
|
+
candidates.append(powershell)
|
|
68
|
+
candidates.append(os.environ.get("COMSPEC") or "cmd.exe")
|
|
69
|
+
return candidates[0]
|
|
70
|
+
|
|
71
|
+
|
|
45
72
|
def _default_cwd() -> str:
|
|
73
|
+
"""Open new terminals in the user's configured projectsDir, else home.
|
|
74
|
+
|
|
75
|
+
The dashboard sends no cwd, so without this a terminal lands in the
|
|
76
|
+
home directory rather than where the operator actually works. Reads
|
|
77
|
+
the existing ~/.arkaos/profile.json projectsDir; falls back to home
|
|
78
|
+
when it is unset or missing (unchanged behaviour for that case).
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
import json
|
|
82
|
+
profile = os.path.join(os.path.expanduser("~"), ".arkaos", "profile.json")
|
|
83
|
+
if os.path.isfile(profile):
|
|
84
|
+
with open(profile, encoding="utf-8") as fh:
|
|
85
|
+
projects_dir = json.load(fh).get("projectsDir")
|
|
86
|
+
if projects_dir and os.path.isdir(projects_dir):
|
|
87
|
+
return projects_dir
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
46
90
|
return os.path.expanduser("~")
|
|
47
91
|
|
|
48
92
|
|
|
@@ -251,14 +295,25 @@ class TerminalSessionManager:
|
|
|
251
295
|
sid = secrets.token_urlsafe(8)
|
|
252
296
|
chosen_shell = shell or _default_shell()
|
|
253
297
|
chosen_cwd = cwd or _default_cwd()
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
298
|
+
if not _PTY_SUPPORTED:
|
|
299
|
+
from core.terminal.session_windows import WindowsTerminalSession
|
|
300
|
+
session = WindowsTerminalSession(
|
|
301
|
+
session_id=sid,
|
|
302
|
+
shell=chosen_shell,
|
|
303
|
+
cwd=chosen_cwd,
|
|
304
|
+
cols=cols,
|
|
305
|
+
rows=rows,
|
|
306
|
+
scrollback_bytes=self.scrollback_bytes,
|
|
307
|
+
)
|
|
308
|
+
else:
|
|
309
|
+
session = TerminalSession(
|
|
310
|
+
session_id=sid,
|
|
311
|
+
shell=chosen_shell,
|
|
312
|
+
cwd=chosen_cwd,
|
|
313
|
+
cols=cols,
|
|
314
|
+
rows=rows,
|
|
315
|
+
scrollback_bytes=self.scrollback_bytes,
|
|
316
|
+
)
|
|
262
317
|
self._sessions[sid] = session
|
|
263
318
|
audit.log_start(sid, chosen_shell, chosen_cwd)
|
|
264
319
|
return session
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Windows PTY session backend (ConPTY via pywinpty).
|
|
2
|
+
|
|
3
|
+
The POSIX backend in ``session.py`` builds on ``pty.fork`` + fcntl /
|
|
4
|
+
termios, none of which exist on Windows. This module provides the same
|
|
5
|
+
``TerminalSession`` surface on top of pywinpty's ConPTY wrapper, so the
|
|
6
|
+
dashboard terminal works on Windows. ``TerminalSessionManager.create``
|
|
7
|
+
selects it when ``os.name == "nt"``.
|
|
8
|
+
|
|
9
|
+
IO model
|
|
10
|
+
--------
|
|
11
|
+
pywinpty's ``read`` is blocking and there is no pollable fd, so the POSIX
|
|
12
|
+
approach (``loop.add_reader(master_fd)``) does not apply. Instead each
|
|
13
|
+
session owns one daemon reader thread that drains the pty for the
|
|
14
|
+
session's whole lifetime: it always appends to the bounded scrollback
|
|
15
|
+
(so a client reconnecting after navigating away replays what it missed)
|
|
16
|
+
and, when a WebSocket is attached, forwards each chunk to that single
|
|
17
|
+
``listener``. Decoupling the reader from the connection avoids two
|
|
18
|
+
readers racing on the same pty across a reload/supersede.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
from typing import Any, Callable, Optional
|
|
26
|
+
|
|
27
|
+
from core.terminal import audit
|
|
28
|
+
|
|
29
|
+
DEFAULT_SCROLLBACK_BYTES = 512 * 1024
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WindowsTerminalSession:
|
|
33
|
+
"""A ConPTY-backed shell + the bookkeeping the manager relies on.
|
|
34
|
+
|
|
35
|
+
Mirrors the public surface of ``session.TerminalSession``:
|
|
36
|
+
``read``/``write``/``resize``/``scrollback``/``is_alive``/``kill``/
|
|
37
|
+
``to_dict`` plus the ``session_id``/``shell``/``cwd``/``exit_code``
|
|
38
|
+
attributes. ``master_fd`` is always ``-1`` — the dashboard WebSocket
|
|
39
|
+
handler uses that to pick the thread-listener pump instead of
|
|
40
|
+
``add_reader``.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
session_id: str,
|
|
46
|
+
shell: str,
|
|
47
|
+
cwd: str,
|
|
48
|
+
cols: int = 120,
|
|
49
|
+
rows: int = 32,
|
|
50
|
+
scrollback_bytes: int = DEFAULT_SCROLLBACK_BYTES,
|
|
51
|
+
) -> None:
|
|
52
|
+
import winpty # lazy: the dependency is only needed on Windows
|
|
53
|
+
|
|
54
|
+
self.session_id = session_id
|
|
55
|
+
self.shell = shell
|
|
56
|
+
self.cwd = cwd
|
|
57
|
+
self.created_at = time.time()
|
|
58
|
+
self.last_activity = time.monotonic()
|
|
59
|
+
self.exit_code: Optional[int] = None
|
|
60
|
+
self.title = ""
|
|
61
|
+
self.scrollback_max = max(0, int(scrollback_bytes))
|
|
62
|
+
self._scrollback = bytearray()
|
|
63
|
+
self._closed = False
|
|
64
|
+
self.master_fd = -1 # no pollable fd on Windows
|
|
65
|
+
|
|
66
|
+
# ConPTY dimensions are (rows, cols).
|
|
67
|
+
self._proc = winpty.PtyProcess.spawn(
|
|
68
|
+
shell,
|
|
69
|
+
cwd=cwd,
|
|
70
|
+
dimensions=(max(1, int(rows)), max(1, int(cols))),
|
|
71
|
+
)
|
|
72
|
+
self.pid = self._proc.pid
|
|
73
|
+
|
|
74
|
+
self._listener: Optional[Callable[[bytes], None]] = None
|
|
75
|
+
self._lock = threading.Lock()
|
|
76
|
+
self._reader = threading.Thread(target=self._read_loop, daemon=True)
|
|
77
|
+
self._reader.start()
|
|
78
|
+
|
|
79
|
+
# -- reader thread ------------------------------------------------------
|
|
80
|
+
def _read_loop(self) -> None:
|
|
81
|
+
"""Drain the pty for the session lifetime; feed scrollback + listener."""
|
|
82
|
+
while not self._closed:
|
|
83
|
+
try:
|
|
84
|
+
chunk = self._proc.read(8192)
|
|
85
|
+
except EOFError:
|
|
86
|
+
break
|
|
87
|
+
except OSError:
|
|
88
|
+
break
|
|
89
|
+
if not chunk:
|
|
90
|
+
if not self._proc_alive():
|
|
91
|
+
break
|
|
92
|
+
continue
|
|
93
|
+
raw = chunk.encode("utf-8", "replace") if isinstance(chunk, str) else bytes(chunk)
|
|
94
|
+
self.last_activity = time.monotonic()
|
|
95
|
+
# Record + capture the listener under the lock so attach() can
|
|
96
|
+
# snapshot scrollback and start receiving with no gap and no dup.
|
|
97
|
+
with self._lock:
|
|
98
|
+
self._record(raw)
|
|
99
|
+
listener = self._listener
|
|
100
|
+
if listener is not None:
|
|
101
|
+
try:
|
|
102
|
+
listener(raw)
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
self._closed = True
|
|
106
|
+
|
|
107
|
+
def attach(self, listener: Callable[[bytes], None]) -> bytes:
|
|
108
|
+
"""Atomically snapshot scrollback and register the live output sink.
|
|
109
|
+
|
|
110
|
+
Returns the scrollback to replay. Any chunk recorded after this
|
|
111
|
+
snapshot is delivered to ``listener`` (no gap, no duplicate).
|
|
112
|
+
"""
|
|
113
|
+
with self._lock:
|
|
114
|
+
snapshot = bytes(self._scrollback)
|
|
115
|
+
self._listener = listener
|
|
116
|
+
return snapshot
|
|
117
|
+
|
|
118
|
+
def set_listener(self, listener: Optional[Callable[[bytes], None]]) -> None:
|
|
119
|
+
"""Register (or clear with ``None``) the live output sink."""
|
|
120
|
+
with self._lock:
|
|
121
|
+
self._listener = listener
|
|
122
|
+
|
|
123
|
+
def _proc_alive(self) -> bool:
|
|
124
|
+
try:
|
|
125
|
+
return self._proc.isalive()
|
|
126
|
+
except OSError:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
# -- output -------------------------------------------------------------
|
|
130
|
+
def read(self, max_bytes: int = 4096) -> bytes:
|
|
131
|
+
"""Compatibility shim: a session-owned thread does the real reading.
|
|
132
|
+
|
|
133
|
+
The POSIX backend is polled via ``read``; on Windows reading happens
|
|
134
|
+
in ``_read_loop``, so direct callers get an empty read.
|
|
135
|
+
"""
|
|
136
|
+
return b""
|
|
137
|
+
|
|
138
|
+
def _record(self, data: bytes) -> None:
|
|
139
|
+
if self.scrollback_max <= 0:
|
|
140
|
+
return
|
|
141
|
+
self._scrollback += data
|
|
142
|
+
if len(self._scrollback) > self.scrollback_max:
|
|
143
|
+
del self._scrollback[: -self.scrollback_max]
|
|
144
|
+
|
|
145
|
+
def scrollback(self) -> bytes:
|
|
146
|
+
return bytes(self._scrollback)
|
|
147
|
+
|
|
148
|
+
# -- input / control ----------------------------------------------------
|
|
149
|
+
def write(self, data: bytes) -> int:
|
|
150
|
+
if self._closed or not data:
|
|
151
|
+
return 0
|
|
152
|
+
text = (
|
|
153
|
+
data.decode("utf-8", "replace")
|
|
154
|
+
if isinstance(data, (bytes, bytearray))
|
|
155
|
+
else str(data)
|
|
156
|
+
)
|
|
157
|
+
try:
|
|
158
|
+
self._proc.write(text)
|
|
159
|
+
except (OSError, EOFError):
|
|
160
|
+
self._closed = True
|
|
161
|
+
return 0
|
|
162
|
+
self.last_activity = time.monotonic()
|
|
163
|
+
return len(data)
|
|
164
|
+
|
|
165
|
+
def resize(self, cols: int, rows: int) -> None:
|
|
166
|
+
if self._closed:
|
|
167
|
+
return
|
|
168
|
+
try:
|
|
169
|
+
self._proc.setwinsize(max(1, int(rows)), max(1, int(cols)))
|
|
170
|
+
except OSError:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
def is_alive(self) -> bool:
|
|
174
|
+
if self._closed:
|
|
175
|
+
return False
|
|
176
|
+
if not self._proc_alive():
|
|
177
|
+
self.exit_code = getattr(self._proc, "exitstatus", None)
|
|
178
|
+
self._closed = True
|
|
179
|
+
return False
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
def kill(self, sig: Optional[int] = None) -> None:
|
|
183
|
+
self._closed = True
|
|
184
|
+
self._listener = None
|
|
185
|
+
try:
|
|
186
|
+
self._proc.terminate(force=True)
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
def _close_fd(self) -> None:
|
|
191
|
+
self._listener = None
|
|
192
|
+
if not self._closed:
|
|
193
|
+
try:
|
|
194
|
+
self._proc.terminate(force=True)
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
self._scrollback.clear()
|
|
198
|
+
self._closed = True
|
|
199
|
+
|
|
200
|
+
def to_dict(self) -> dict[str, Any]:
|
|
201
|
+
idle_s = max(0.0, time.monotonic() - self.last_activity)
|
|
202
|
+
return {
|
|
203
|
+
"session_id": self.session_id,
|
|
204
|
+
"shell": self.shell,
|
|
205
|
+
"cwd": self.cwd,
|
|
206
|
+
"title": self.title or self.session_id,
|
|
207
|
+
"created_at": self.created_at,
|
|
208
|
+
"idle_seconds": round(idle_s, 1),
|
|
209
|
+
"alive": self.is_alive(),
|
|
210
|
+
"exit_code": self.exit_code,
|
|
211
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -42,6 +42,7 @@ from dataclasses import dataclass
|
|
|
42
42
|
from pathlib import Path
|
|
43
43
|
|
|
44
44
|
from core.shared import safe_session_id as _safe_session_id_module
|
|
45
|
+
from core.shared.temp_paths import arkaos_temp_dir
|
|
45
46
|
|
|
46
47
|
_safe_session_id = _safe_session_id_module.safe_session_id
|
|
47
48
|
|
|
@@ -63,7 +64,7 @@ class GraceState:
|
|
|
63
64
|
|
|
64
65
|
def _base_dir() -> Path:
|
|
65
66
|
override = os.environ.get("ARKA_FLOW_AUTH_DIR", "").strip()
|
|
66
|
-
return Path(override) if override else
|
|
67
|
+
return Path(override) if override else arkaos_temp_dir("arkaos-flow-auth")
|
|
67
68
|
|
|
68
69
|
|
|
69
70
|
def _auth_path(session_id: str) -> Path | None:
|
|
@@ -25,6 +25,7 @@ from datetime import datetime, timezone
|
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
|
|
27
27
|
from core.shared import safe_session_id as _safe_session_id_module
|
|
28
|
+
from core.shared.temp_paths import arkaos_temp_dir
|
|
28
29
|
from core.workflow import flow_authorization, marker_cache
|
|
29
30
|
|
|
30
31
|
try:
|
|
@@ -210,7 +211,7 @@ ASSISTANT_WINDOW = 20
|
|
|
210
211
|
CONFIG_PATH = Path.home() / ".arkaos" / "config.json"
|
|
211
212
|
BYPASS_AUDIT_PATH = Path.home() / ".arkaos" / "audit" / "bypass.log"
|
|
212
213
|
TELEMETRY_PATH = Path.home() / ".arkaos" / "telemetry" / "enforcement.jsonl"
|
|
213
|
-
FLOW_REQUIRED_DIR =
|
|
214
|
+
FLOW_REQUIRED_DIR = arkaos_temp_dir("arkaos-wf-required")
|
|
214
215
|
|
|
215
216
|
|
|
216
217
|
@dataclass
|
|
@@ -20,13 +20,14 @@ from dataclasses import dataclass
|
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
|
|
22
22
|
from core.shared import safe_session_id as _safe_session_id_module
|
|
23
|
+
from core.shared.temp_paths import arkaos_temp_dir
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def _resolve_cache_dir() -> Path:
|
|
26
27
|
override = os.environ.get("ARKA_MARKER_CACHE_DIR", "").strip()
|
|
27
28
|
if override:
|
|
28
29
|
return Path(override)
|
|
29
|
-
return
|
|
30
|
+
return arkaos_temp_dir("arkaos-flow-marker")
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
MARKER_CACHE_DIR = _resolve_cache_dir()
|
|
@@ -29,6 +29,7 @@ from datetime import datetime, timezone
|
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
|
|
31
31
|
from core.shared import safe_session_id as _safe_session_id_module
|
|
32
|
+
from core.shared.temp_paths import arkaos_temp_dir
|
|
32
33
|
from core.synapse import kb_cache
|
|
33
34
|
|
|
34
35
|
try:
|
|
@@ -55,7 +56,7 @@ SAFE_SESSION_ID_RE = _safe_session_id_module.SAFE_SESSION_ID_RE
|
|
|
55
56
|
CONFIG_PATH = Path.home() / ".arkaos" / "config.json"
|
|
56
57
|
BYPASS_AUDIT_PATH = Path.home() / ".arkaos" / "audit" / "kb_first_bypass.log"
|
|
57
58
|
TELEMETRY_PATH = Path.home() / ".arkaos" / "telemetry" / "kb_first.jsonl"
|
|
58
|
-
VIOLATION_DIR =
|
|
59
|
+
VIOLATION_DIR = arkaos_temp_dir("arkaos-kb-violation")
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
def _violation_dir() -> Path:
|
|
@@ -131,9 +131,14 @@ function formatIncidentTs(iso: string): string {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
const toast = useToast()
|
|
134
135
|
function copyCommand(cmd: string) {
|
|
135
136
|
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
136
|
-
navigator.clipboard.writeText(cmd)
|
|
137
|
+
navigator.clipboard.writeText(cmd)
|
|
138
|
+
.then(() => toast.add({ title: 'Copied to clipboard', description: cmd, icon: 'i-lucide-clipboard-check' }))
|
|
139
|
+
.catch(() => toast.add({ title: 'Copy failed', description: cmd, color: 'error', icon: 'i-lucide-clipboard-x' }))
|
|
140
|
+
} else {
|
|
141
|
+
toast.add({ title: 'Clipboard unavailable', description: cmd, color: 'warning' })
|
|
137
142
|
}
|
|
138
143
|
}
|
|
139
144
|
</script>
|
|
@@ -40,7 +40,7 @@ Manage Model Context Protocol (MCP) servers for projects. MCPs extend Claude Cod
|
|
|
40
40
|
|
|
41
41
|
| Profile | MCPs Included | Use For |
|
|
42
42
|
|---------|---------------|---------|
|
|
43
|
-
| `base` | obsidian, context7, playwright,
|
|
43
|
+
| `base` | obsidian, context7, playwright, sentry, gh-grep, clickup, firecrawl, supabase | All projects |
|
|
44
44
|
| `laravel` | base + laravel-boost, serena | Laravel backends |
|
|
45
45
|
| `nuxt` | base + nuxt, nuxt-ui | Nuxt 3/4 apps |
|
|
46
46
|
| `vue` | base + nuxt-ui | Vue 3 SPAs |
|
|
@@ -50,6 +50,8 @@ Manage Model Context Protocol (MCP) servers for projects. MCPs extend Claude Cod
|
|
|
50
50
|
| `full-stack` | base + laravel-boost, serena, nuxt, nuxt-ui | Laravel + Nuxt apps |
|
|
51
51
|
| `comms` | base + slack, discord, whatsapp, teams | Messaging platforms |
|
|
52
52
|
|
|
53
|
+
> Runtime-managed MCPs (not a profile): `claude-in-chrome` (extension) and `claude-mem` (plugin) are registered in the registry for governance and telemetry only — they have no launchable command and are never written to `.mcp.json`.
|
|
54
|
+
|
|
53
55
|
## How It Works
|
|
54
56
|
|
|
55
57
|
### /dev mcp apply <profile>
|
|
@@ -31,7 +31,8 @@ Deduplicate by absolute path. Skip projects whose path does not exist.
|
|
|
31
31
|
### Phase 3a — MCP Sync
|
|
32
32
|
For each project: regenerate `.mcp.json` based on the registry + detected stack.
|
|
33
33
|
|
|
34
|
-
Base MCPs (all projects): `arka-prompts`, `context7`, `obsidian`, `clickup`, `
|
|
34
|
+
Base MCPs (all projects): `arka-prompts`, `context7`, `obsidian`, `clickup`, `playwright`, `gh-grep`.
|
|
35
|
+
Runtime-managed entries (`claude-in-chrome`, `claude-mem`) are registered for governance and telemetry only and are never written to `.mcp.json`; `memory-bank` is `optional` (demoted from base 2026-07-08).
|
|
35
36
|
|
|
36
37
|
Stack additions:
|
|
37
38
|
- Laravel: `laravel-boost`, `serena`, `sentry`
|
|
@@ -5,6 +5,8 @@ department: quality
|
|
|
5
5
|
tier: 0
|
|
6
6
|
# PR-4 evidence QG (v4.1): reviewers run sonnet by default; dispatch on
|
|
7
7
|
# opus ONLY for Tier 0/security-scope diffs (see constitution model-routing).
|
|
8
|
+
# Excellence Reform (v4.2): the Model Fabric quality_gate role upgrades this
|
|
9
|
+
# at dispatch time (~/.arkaos/models.yaml) — sonnet here is the floor, not the route.
|
|
8
10
|
model: sonnet
|
|
9
11
|
|
|
10
12
|
behavioral_dna:
|
|
@@ -5,6 +5,8 @@ department: quality
|
|
|
5
5
|
tier: 0
|
|
6
6
|
# PR-4 evidence QG (v4.1): reviewers run sonnet by default; dispatch on
|
|
7
7
|
# opus ONLY for Tier 0/security-scope diffs (see constitution model-routing).
|
|
8
|
+
# Excellence Reform (v4.2): the Model Fabric quality_gate role upgrades this
|
|
9
|
+
# at dispatch time (~/.arkaos/models.yaml) — sonnet here is the floor, not the route.
|
|
8
10
|
model: sonnet
|
|
9
11
|
|
|
10
12
|
behavioral_dna:
|
package/installer/cli.js
CHANGED
|
@@ -152,7 +152,7 @@ async function main() {
|
|
|
152
152
|
const repoRootDash = join(__dirname, "..");
|
|
153
153
|
const dashCmd = IS_WINDOWS
|
|
154
154
|
? `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${join(repoRootDash, "scripts", "start-dashboard.ps1")}"`
|
|
155
|
-
: `bash "${repoRootDash
|
|
155
|
+
: `bash "${join(repoRootDash, "scripts", "start-dashboard.sh")}"`;
|
|
156
156
|
try {
|
|
157
157
|
execDash(dashCmd, {
|
|
158
158
|
stdio: "inherit",
|
|
@@ -199,13 +199,14 @@ async function main() {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
case "index": {
|
|
202
|
-
const {
|
|
203
|
-
const indexArgs = positionals.slice(1).join(" ");
|
|
202
|
+
const { execFileSync } = await import("node:child_process");
|
|
204
203
|
const repoRoot = join(__dirname, "..");
|
|
205
204
|
const pyIndex = getArkaosPython();
|
|
206
205
|
if (!pyIndex) { console.error("No Python found. Run: npx arkaos install"); process.exit(1); }
|
|
207
206
|
try {
|
|
208
|
-
|
|
207
|
+
// argv array, not a joined string: vault paths with spaces
|
|
208
|
+
// ("C:\Users\Ana Maria\vault") broke the quoted-string form.
|
|
209
|
+
execFileSync(pyIndex, [join(repoRoot, "scripts", "knowledge-index.py"), ...positionals.slice(1)], {
|
|
209
210
|
stdio: "inherit",
|
|
210
211
|
env: { ...process.env, ARKAOS_ROOT: repoRoot },
|
|
211
212
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
cpSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
copyFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { join, basename } from "node:path";
|
|
10
|
+
|
|
11
|
+
// Deploys a stable snapshot of the Python core package (+ VERSION) into
|
|
12
|
+
// ~/.arkaos/lib. `.repo-path` points at whichever npx cache last ran an
|
|
13
|
+
// install/update — a location `npm cache clean` can purge at any time.
|
|
14
|
+
// When that happens, every `arka-py -m core.*` entrypoint (hooks,
|
|
15
|
+
// /arka update, telemetry CLIs) loses the core package unless it runs
|
|
16
|
+
// from a dev checkout. The snapshot is the always-present fallback that
|
|
17
|
+
// bin/arka-py and core/hooks/_shared.py validate against.
|
|
18
|
+
//
|
|
19
|
+
// core/sync/__init__.py is the validation marker: it distinguishes the
|
|
20
|
+
// full package from the cognitive scheduler's minimal core/ copy in
|
|
21
|
+
// ~/.arkaos/core (cognition + workflow only).
|
|
22
|
+
//
|
|
23
|
+
// Shared by installer/index.js (fresh install) and installer/update.js —
|
|
24
|
+
// same single-implementation rationale as hook-lib.js: the v4.3.2
|
|
25
|
+
// regression existed because two deploy loops drifted.
|
|
26
|
+
export function deployCoreSnapshot(arkaosRoot, installDir) {
|
|
27
|
+
const srcCore = join(arkaosRoot, "core");
|
|
28
|
+
if (!existsSync(join(srcCore, "sync", "__init__.py"))) return false;
|
|
29
|
+
|
|
30
|
+
const libDir = join(installDir, "lib");
|
|
31
|
+
const destCore = join(libDir, "core");
|
|
32
|
+
const staging = join(libDir, ".core.staging");
|
|
33
|
+
const previous = join(libDir, ".core.previous");
|
|
34
|
+
|
|
35
|
+
// Stage + swap so a crash at any point never destroys the last good
|
|
36
|
+
// snapshot: the new tree is fully written to staging first, the old
|
|
37
|
+
// snapshot is only moved aside (not deleted) before the swap, and its
|
|
38
|
+
// removal is the final step.
|
|
39
|
+
rmSync(staging, { recursive: true, force: true });
|
|
40
|
+
rmSync(previous, { recursive: true, force: true });
|
|
41
|
+
mkdirSync(staging, { recursive: true });
|
|
42
|
+
cpSync(srcCore, staging, {
|
|
43
|
+
recursive: true,
|
|
44
|
+
filter: (src) => basename(src) !== "__pycache__",
|
|
45
|
+
});
|
|
46
|
+
if (existsSync(destCore)) renameSync(destCore, previous);
|
|
47
|
+
renameSync(staging, destCore);
|
|
48
|
+
rmSync(previous, { recursive: true, force: true });
|
|
49
|
+
|
|
50
|
+
const versionFile = join(arkaosRoot, "VERSION");
|
|
51
|
+
if (existsSync(versionFile)) copyFileSync(versionFile, join(libDir, "VERSION"));
|
|
52
|
+
return true;
|
|
53
|
+
}
|
package/installer/doctor.js
CHANGED
|
@@ -99,15 +99,20 @@ const checks = [
|
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
name: "repo-path",
|
|
102
|
-
description: "
|
|
102
|
+
description: "Python core reachable (.repo-path or ~/.arkaos/lib snapshot)",
|
|
103
103
|
severity: "warn",
|
|
104
104
|
check: () => {
|
|
105
|
+
// The stable snapshot keeps arka-py working even after
|
|
106
|
+
// `npm cache clean` purges the npx dir .repo-path points at.
|
|
107
|
+
if (existsSync(join(INSTALL_DIR, "lib", "core", "sync", "__init__.py"))) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
105
110
|
const p = join(INSTALL_DIR, ".repo-path");
|
|
106
111
|
if (!existsSync(p)) return false;
|
|
107
112
|
const root = readFileSync(p, "utf-8").trim();
|
|
108
|
-
return existsSync(root);
|
|
113
|
+
return existsSync(join(root, "core", "sync", "__init__.py"));
|
|
109
114
|
},
|
|
110
|
-
fix: () => "Run: npx arkaos
|
|
115
|
+
fix: () => "Run: npx arkaos@latest update (recreates the ~/.arkaos/lib core snapshot)",
|
|
111
116
|
},
|
|
112
117
|
{
|
|
113
118
|
name: "profile",
|
package/installer/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getRuntimeConfig } from "./detect-runtime.js";
|
|
|
7
7
|
import { findSystemPython, ensureVenv, getArkaosPython, getArkaosPip, pipInstall } from "./python-resolver.js";
|
|
8
8
|
import { IS_WINDOWS, HOOK_EXT } from "./platform.js";
|
|
9
9
|
import { copyHookLib } from "./hook-lib.js";
|
|
10
|
+
import { deployCoreSnapshot } from "./core-snapshot.js";
|
|
10
11
|
|
|
11
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
13
|
const __dirname = dirname(__filename);
|
|
@@ -211,6 +212,17 @@ export async function install({ runtime, path, force, skipSystem, withOllama })
|
|
|
211
212
|
// ═══ Step 11: Create references and profile ═══
|
|
212
213
|
step(11, 14, "Creating references...");
|
|
213
214
|
writeFileSync(join(installDir, ".repo-path"), ARKAOS_ROOT);
|
|
215
|
+
// .repo-path points at the npx cache, which `npm cache clean` can purge;
|
|
216
|
+
// the ~/.arkaos/lib snapshot is the validated fallback for arka-py and
|
|
217
|
+
// the Python hooks (see installer/core-snapshot.js). A failed snapshot
|
|
218
|
+
// must never fail the install — the resolvers degrade to .repo-path.
|
|
219
|
+
try {
|
|
220
|
+
if (deployCoreSnapshot(ARKAOS_ROOT, installDir)) {
|
|
221
|
+
ok("Core snapshot deployed to ~/.arkaos/lib (survives npx cache purges)");
|
|
222
|
+
}
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.log(` ⚠ Core snapshot skipped (${err.message}) — arka-py falls back to .repo-path`);
|
|
225
|
+
}
|
|
214
226
|
const skillsDir = join(config.skillsDir || join(homedir(), ".claude", "skills"), "arkaos");
|
|
215
227
|
ensureDir(skillsDir);
|
|
216
228
|
writeFileSync(join(skillsDir, ".arkaos-root"), ARKAOS_ROOT);
|
|
@@ -520,8 +532,12 @@ function installAllPythonDeps(userConfig = {}) {
|
|
|
520
532
|
// a failure of one does not block the other.
|
|
521
533
|
// Ingest (YouTube, PDF, web, audio)
|
|
522
534
|
const ingestDeps = "yt-dlp pdfplumber beautifulsoup4 requests";
|
|
523
|
-
// Dashboard API
|
|
524
|
-
|
|
535
|
+
// Dashboard API. python-multipart is required by FastAPI for the
|
|
536
|
+
// knowledge upload endpoint (its absence fails the API import on every
|
|
537
|
+
// platform). pywinpty backs the dashboard terminal on Windows (ConPTY).
|
|
538
|
+
const dashboardDeps = process.platform === "win32"
|
|
539
|
+
? "fastapi uvicorn python-multipart pywinpty"
|
|
540
|
+
: "fastapi uvicorn python-multipart";
|
|
525
541
|
// Transcription
|
|
526
542
|
const transcriptionDeps = "faster-whisper";
|
|
527
543
|
|
|
@@ -563,8 +579,8 @@ function installAllPythonDeps(userConfig = {}) {
|
|
|
563
579
|
// Dashboard deps (optional)
|
|
564
580
|
if (userConfig.installDashboard !== false) {
|
|
565
581
|
console.log(" Installing dashboard dependencies...");
|
|
566
|
-
if (pipInstall(dashboardDeps, { log, timeout:
|
|
567
|
-
ok("Dashboard API installed (fastapi, uvicorn)");
|
|
582
|
+
if (pipInstall(dashboardDeps, { log, timeout: 120000 })) {
|
|
583
|
+
ok("Dashboard API installed (fastapi, uvicorn, python-multipart)");
|
|
568
584
|
} else {
|
|
569
585
|
warn("Dashboard API not installed (optional)");
|
|
570
586
|
}
|
|
@@ -593,12 +609,19 @@ function copyConfigFiles(installDir) {
|
|
|
593
609
|
["config/constitution.yaml", "config/constitution.yaml"],
|
|
594
610
|
];
|
|
595
611
|
|
|
596
|
-
// Standards
|
|
612
|
+
// Standards — files copied individually; subdirectories (e.g. claude-md-overlays)
|
|
613
|
+
// use cpSync recursively because copyFileSync fails with EPERM on directories.
|
|
597
614
|
const standardsDir = join(ARKAOS_ROOT, "config", "standards");
|
|
598
615
|
if (existsSync(standardsDir)) {
|
|
599
616
|
ensureDir(join(installDir, "config", "standards"));
|
|
600
617
|
for (const f of readdirSync(standardsDir)) {
|
|
601
|
-
|
|
618
|
+
const srcEntry = join(standardsDir, f);
|
|
619
|
+
const destEntry = join(installDir, "config", "standards", f);
|
|
620
|
+
if (statSync(srcEntry).isDirectory()) {
|
|
621
|
+
cpSync(srcEntry, destEntry, { recursive: true });
|
|
622
|
+
} else {
|
|
623
|
+
files.push([`config/standards/${f}`, `config/standards/${f}`]);
|
|
624
|
+
}
|
|
602
625
|
}
|
|
603
626
|
}
|
|
604
627
|
|