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.
Files changed (110) hide show
  1. package/VERSION +1 -1
  2. package/bin/arka-py +22 -7
  3. package/config/hooks/_lib/arka_python.ps1 +31 -0
  4. package/config/hooks/_lib/arka_python.sh +38 -0
  5. package/config/hooks/post-tool-use.ps1 +8 -0
  6. package/config/hooks/post-tool-use.sh +11 -5
  7. package/config/hooks/pre-tool-use.ps1 +11 -6
  8. package/config/hooks/pre-tool-use.sh +11 -6
  9. package/config/hooks/session-start.ps1 +10 -0
  10. package/config/hooks/session-start.sh +5 -0
  11. package/config/hooks/stop.ps1 +9 -1
  12. package/config/hooks/stop.sh +11 -5
  13. package/config/hooks/user-prompt-submit.ps1 +18 -0
  14. package/config/hooks/user-prompt-submit.sh +11 -5
  15. package/config/mcp-policy.yaml +8 -8
  16. package/core/agents/__pycache__/__init__.cpython-312.pyc +0 -0
  17. package/core/agents/__pycache__/loader.cpython-312.pyc +0 -0
  18. package/core/agents/__pycache__/registry_gen.cpython-313.pyc +0 -0
  19. package/core/agents/__pycache__/schema.cpython-312.pyc +0 -0
  20. package/core/agents/registry_gen.py +3 -2
  21. package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
  22. package/core/cognition/scheduler/daemon.py +7 -3
  23. package/core/forge/__pycache__/orchestrator.cpython-313.pyc +0 -0
  24. package/core/forge/orchestrator.py +2 -1
  25. package/core/hooks/__pycache__/_shared.cpython-313.pyc +0 -0
  26. package/core/hooks/__pycache__/_shared.cpython-314.pyc +0 -0
  27. package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
  28. package/core/hooks/__pycache__/stop.cpython-314.pyc +0 -0
  29. package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  30. package/core/hooks/_shared.py +29 -9
  31. package/core/hooks/stop.py +4 -3
  32. package/core/hooks/user_prompt_submit.py +5 -4
  33. package/core/knowledge/__pycache__/indexer.cpython-313.pyc +0 -0
  34. package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
  35. package/core/knowledge/indexer.py +1 -1
  36. package/core/knowledge/vector_store.py +10 -3
  37. package/core/runtime/__pycache__/model_routing_check.cpython-313.pyc +0 -0
  38. package/core/runtime/__pycache__/model_routing_context.cpython-313.pyc +0 -0
  39. package/core/runtime/__pycache__/native_usage.cpython-313.pyc +0 -0
  40. package/core/runtime/__pycache__/native_usage.cpython-314.pyc +0 -0
  41. package/core/runtime/gateway/__pycache__/__init__.cpython-313.pyc +0 -0
  42. package/core/runtime/gateway/__pycache__/__main__.cpython-313.pyc +0 -0
  43. package/core/runtime/gateway/__pycache__/litellm_config.cpython-313.pyc +0 -0
  44. package/core/runtime/native_usage.py +2 -1
  45. package/core/shared/__pycache__/temp_paths.cpython-313.pyc +0 -0
  46. package/core/shared/__pycache__/temp_paths.cpython-314.pyc +0 -0
  47. package/core/shared/temp_paths.py +40 -0
  48. package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
  49. package/core/synapse/__pycache__/kb_cache.cpython-314.pyc +0 -0
  50. package/core/synapse/kb_cache.py +3 -2
  51. package/core/sync/__pycache__/agent_provisioner.cpython-313.pyc +0 -0
  52. package/core/sync/__pycache__/ai_mcp_decider.cpython-313.pyc +0 -0
  53. package/core/sync/__pycache__/content_syncer.cpython-313.pyc +0 -0
  54. package/core/sync/__pycache__/descriptor_syncer.cpython-313.pyc +0 -0
  55. package/core/sync/__pycache__/discovery.cpython-313.pyc +0 -0
  56. package/core/sync/__pycache__/engine.cpython-313.pyc +0 -0
  57. package/core/sync/__pycache__/manifest.cpython-313.pyc +0 -0
  58. package/core/sync/__pycache__/mcp_optimizer.cpython-313.pyc +0 -0
  59. package/core/sync/__pycache__/mcp_syncer.cpython-313.pyc +0 -0
  60. package/core/sync/__pycache__/policy_loader.cpython-313.pyc +0 -0
  61. package/core/sync/__pycache__/reporter.cpython-313.pyc +0 -0
  62. package/core/sync/__pycache__/settings_syncer.cpython-313.pyc +0 -0
  63. package/core/sync/agent_provisioner.py +5 -5
  64. package/core/sync/ai_mcp_decider.py +2 -2
  65. package/core/sync/content_syncer.py +14 -14
  66. package/core/sync/descriptor_syncer.py +2 -2
  67. package/core/sync/discovery.py +4 -4
  68. package/core/sync/engine.py +4 -4
  69. package/core/sync/manifest.py +1 -1
  70. package/core/sync/mcp_optimizer.py +5 -5
  71. package/core/sync/mcp_syncer.py +7 -3
  72. package/core/sync/policy_loader.py +1 -1
  73. package/core/sync/reporter.py +1 -1
  74. package/core/sync/settings_syncer.py +2 -2
  75. package/core/terminal/__pycache__/session.cpython-313.pyc +0 -0
  76. package/core/terminal/__pycache__/session_windows.cpython-313.pyc +0 -0
  77. package/core/terminal/session.py +66 -11
  78. package/core/terminal/session_windows.py +211 -0
  79. package/core/workflow/__pycache__/flow_authorization.cpython-313.pyc +0 -0
  80. package/core/workflow/__pycache__/flow_authorization.cpython-314.pyc +0 -0
  81. package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
  82. package/core/workflow/__pycache__/flow_enforcer.cpython-314.pyc +0 -0
  83. package/core/workflow/__pycache__/marker_cache.cpython-313.pyc +0 -0
  84. package/core/workflow/__pycache__/marker_cache.cpython-314.pyc +0 -0
  85. package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
  86. package/core/workflow/__pycache__/research_gate.cpython-314.pyc +0 -0
  87. package/core/workflow/flow_authorization.py +2 -1
  88. package/core/workflow/flow_enforcer.py +2 -1
  89. package/core/workflow/marker_cache.py +2 -1
  90. package/core/workflow/research_gate.py +2 -1
  91. package/dashboard/app/pages/index.vue +6 -1
  92. package/departments/dev/skills/mcp/SKILL.md +3 -1
  93. package/departments/ops/skills/update/references/sync-engine.md +2 -1
  94. package/departments/quality/agents/copy-director.yaml +2 -0
  95. package/departments/quality/agents/tech-director.yaml +2 -0
  96. package/installer/cli.js +5 -4
  97. package/installer/core-snapshot.js +53 -0
  98. package/installer/doctor.js +8 -3
  99. package/installer/index.js +29 -6
  100. package/installer/migrate.js +7 -4
  101. package/installer/update.js +13 -0
  102. package/knowledge/agents-registry-v2.json +487 -120
  103. package/package.json +1 -1
  104. package/pyproject.toml +2 -1
  105. package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
  106. package/scripts/__pycache__/synapse-bridge.cpython-313.pyc +0 -0
  107. package/scripts/dashboard-api.py +69 -33
  108. package/scripts/knowledge-index.py +35 -2
  109. package/scripts/start-dashboard.ps1 +34 -14
  110. package/scripts/synapse-bridge.py +3 -1
@@ -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
- session = TerminalSession(
255
- session_id=sid,
256
- shell=chosen_shell,
257
- cwd=chosen_cwd,
258
- cols=cols,
259
- rows=rows,
260
- scrollback_bytes=self.scrollback_bytes,
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
+ }
@@ -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 Path("/tmp/arkaos-flow-auth")
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 = Path("/tmp/arkaos-wf-required")
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 Path("/tmp/arkaos-flow-marker")
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 = Path("/tmp/arkaos-kb-violation")
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).catch(() => { /* ignore */ })
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, memory-bank, sentry, gh-grep, clickup, firecrawl, supabase | All projects |
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`, `memory-bank`, `playwright`, `gh-grep`.
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}/scripts/start-dashboard.sh"`;
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 { execSync } = await import("node:child_process");
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
- execSync(`"${pyIndex}" "${join(repoRoot, "scripts", "knowledge-index.py")}" ${indexArgs || ""}`, {
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
+ }
@@ -99,15 +99,20 @@ const checks = [
99
99
  },
100
100
  {
101
101
  name: "repo-path",
102
- description: "Repo path reference exists",
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 install --force",
115
+ fix: () => "Run: npx arkaos@latest update (recreates the ~/.arkaos/lib core snapshot)",
111
116
  },
112
117
  {
113
118
  name: "profile",
@@ -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
- const dashboardDeps = "fastapi uvicorn";
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: 60000 })) {
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
- files.push([`config/standards/${f}`, `config/standards/${f}`]);
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