arkaos 4.3.6 → 4.4.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 (99) 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/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
  16. package/core/cognition/scheduler/daemon.py +7 -3
  17. package/core/forge/__pycache__/orchestrator.cpython-313.pyc +0 -0
  18. package/core/forge/orchestrator.py +2 -1
  19. package/core/hooks/__pycache__/_shared.cpython-313.pyc +0 -0
  20. package/core/hooks/__pycache__/_shared.cpython-314.pyc +0 -0
  21. package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
  22. package/core/hooks/__pycache__/stop.cpython-314.pyc +0 -0
  23. package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  24. package/core/hooks/_shared.py +29 -9
  25. package/core/hooks/stop.py +4 -3
  26. package/core/hooks/user_prompt_submit.py +5 -4
  27. package/core/knowledge/__pycache__/indexer.cpython-313.pyc +0 -0
  28. package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
  29. package/core/knowledge/indexer.py +1 -1
  30. package/core/knowledge/vector_store.py +10 -3
  31. package/core/runtime/__pycache__/model_routing_check.cpython-313.pyc +0 -0
  32. package/core/runtime/__pycache__/model_routing_context.cpython-313.pyc +0 -0
  33. package/core/runtime/__pycache__/native_usage.cpython-313.pyc +0 -0
  34. package/core/runtime/__pycache__/native_usage.cpython-314.pyc +0 -0
  35. package/core/runtime/gateway/__pycache__/__init__.cpython-313.pyc +0 -0
  36. package/core/runtime/gateway/__pycache__/__main__.cpython-313.pyc +0 -0
  37. package/core/runtime/gateway/__pycache__/litellm_config.cpython-313.pyc +0 -0
  38. package/core/runtime/native_usage.py +2 -1
  39. package/core/shared/__pycache__/temp_paths.cpython-313.pyc +0 -0
  40. package/core/shared/__pycache__/temp_paths.cpython-314.pyc +0 -0
  41. package/core/shared/temp_paths.py +40 -0
  42. package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
  43. package/core/synapse/__pycache__/kb_cache.cpython-314.pyc +0 -0
  44. package/core/synapse/kb_cache.py +3 -2
  45. package/core/sync/__pycache__/agent_provisioner.cpython-313.pyc +0 -0
  46. package/core/sync/__pycache__/ai_mcp_decider.cpython-313.pyc +0 -0
  47. package/core/sync/__pycache__/content_syncer.cpython-313.pyc +0 -0
  48. package/core/sync/__pycache__/descriptor_syncer.cpython-313.pyc +0 -0
  49. package/core/sync/__pycache__/discovery.cpython-313.pyc +0 -0
  50. package/core/sync/__pycache__/engine.cpython-313.pyc +0 -0
  51. package/core/sync/__pycache__/manifest.cpython-313.pyc +0 -0
  52. package/core/sync/__pycache__/mcp_optimizer.cpython-313.pyc +0 -0
  53. package/core/sync/__pycache__/mcp_syncer.cpython-313.pyc +0 -0
  54. package/core/sync/__pycache__/policy_loader.cpython-313.pyc +0 -0
  55. package/core/sync/__pycache__/reporter.cpython-313.pyc +0 -0
  56. package/core/sync/__pycache__/settings_syncer.cpython-313.pyc +0 -0
  57. package/core/sync/agent_provisioner.py +5 -5
  58. package/core/sync/ai_mcp_decider.py +2 -2
  59. package/core/sync/content_syncer.py +14 -14
  60. package/core/sync/descriptor_syncer.py +2 -2
  61. package/core/sync/discovery.py +4 -4
  62. package/core/sync/engine.py +4 -4
  63. package/core/sync/manifest.py +1 -1
  64. package/core/sync/mcp_optimizer.py +5 -5
  65. package/core/sync/mcp_syncer.py +3 -3
  66. package/core/sync/policy_loader.py +1 -1
  67. package/core/sync/reporter.py +1 -1
  68. package/core/sync/settings_syncer.py +2 -2
  69. package/core/terminal/__pycache__/session.cpython-313.pyc +0 -0
  70. package/core/terminal/__pycache__/session_windows.cpython-313.pyc +0 -0
  71. package/core/terminal/session.py +66 -11
  72. package/core/terminal/session_windows.py +211 -0
  73. package/core/workflow/__pycache__/flow_authorization.cpython-313.pyc +0 -0
  74. package/core/workflow/__pycache__/flow_authorization.cpython-314.pyc +0 -0
  75. package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
  76. package/core/workflow/__pycache__/flow_enforcer.cpython-314.pyc +0 -0
  77. package/core/workflow/__pycache__/marker_cache.cpython-313.pyc +0 -0
  78. package/core/workflow/__pycache__/marker_cache.cpython-314.pyc +0 -0
  79. package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
  80. package/core/workflow/__pycache__/research_gate.cpython-314.pyc +0 -0
  81. package/core/workflow/flow_authorization.py +2 -1
  82. package/core/workflow/flow_enforcer.py +2 -1
  83. package/core/workflow/marker_cache.py +2 -1
  84. package/core/workflow/research_gate.py +2 -1
  85. package/dashboard/app/pages/index.vue +6 -1
  86. package/installer/cli.js +5 -4
  87. package/installer/core-snapshot.js +53 -0
  88. package/installer/doctor.js +8 -3
  89. package/installer/index.js +29 -6
  90. package/installer/migrate.js +7 -4
  91. package/installer/update.js +13 -0
  92. package/package.json +1 -1
  93. package/pyproject.toml +2 -1
  94. package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
  95. package/scripts/__pycache__/synapse-bridge.cpython-313.pyc +0 -0
  96. package/scripts/dashboard-api.py +69 -33
  97. package/scripts/knowledge-index.py +35 -2
  98. package/scripts/start-dashboard.ps1 +34 -14
  99. package/scripts/synapse-bridge.py +3 -1
@@ -20,6 +20,8 @@ from typing import Optional
20
20
  ARKAOS_ROOT = Path(os.environ.get("ARKAOS_ROOT", Path(__file__).resolve().parent.parent))
21
21
  sys.path.insert(0, str(ARKAOS_ROOT))
22
22
 
23
+ from core.shared.temp_paths import arkaos_temp_dir
24
+
23
25
  from fastapi import FastAPI, Query, Request, WebSocket, WebSocketDisconnect
24
26
  from fastapi.middleware.cors import CORSMiddleware
25
27
  from fastapi.responses import FileResponse, JSONResponse
@@ -158,10 +160,10 @@ def overview():
158
160
 
159
161
  skills_count = 0
160
162
  try:
161
- skills_count = int(subprocess.run(
162
- ["find", str(ARKAOS_ROOT / "departments"), "-name", "SKILL.md", "-path", "*/skills/*/SKILL.md"],
163
- capture_output=True, text=True, timeout=5,
164
- ).stdout.strip().count("\n")) + 1
163
+ skills_count = sum(
164
+ 1 for p in (ARKAOS_ROOT / "departments").rglob("SKILL.md")
165
+ if "skills" in p.parts
166
+ )
165
167
  except Exception:
166
168
  skills_count = 250
167
169
 
@@ -1733,7 +1735,7 @@ def health():
1733
1735
  "npx arkaos install")
1734
1736
 
1735
1737
  try:
1736
- subprocess.run(["python3", "--version"], capture_output=True, timeout=2)
1738
+ subprocess.run([sys.executable, "--version"], capture_output=True, timeout=2)
1737
1739
  check("python", True)
1738
1740
  except Exception:
1739
1741
  check("python", False, "Install Python 3.11+")
@@ -2794,6 +2796,18 @@ def _current_version() -> str:
2794
2796
  return "0.0.0"
2795
2797
 
2796
2798
 
2799
+ def _win_shim(cmd: list) -> list:
2800
+ """Wrap npm/npx-style commands so Windows can execute them.
2801
+
2802
+ npm and npx are ``.cmd`` shims; ``subprocess`` (CreateProcess) cannot
2803
+ run them directly ("%1 is not a valid Win32 application"), so route
2804
+ through ``cmd /c`` on Windows. No-op on POSIX.
2805
+ """
2806
+ if os.name == "nt":
2807
+ return ["cmd", "/c", *cmd]
2808
+ return cmd
2809
+
2810
+
2797
2811
  def _npm_latest_version():
2798
2812
  import subprocess
2799
2813
  import time
@@ -2802,7 +2816,7 @@ def _npm_latest_version():
2802
2816
  return _npm_latest_cache["version"]
2803
2817
  try:
2804
2818
  out = subprocess.run(
2805
- ["npm", "view", "arkaos", "version"],
2819
+ _win_shim(["npm", "view", "arkaos", "version"]),
2806
2820
  capture_output=True, text=True, timeout=20,
2807
2821
  )
2808
2822
  latest = (out.stdout or "").strip() or None
@@ -2829,7 +2843,7 @@ def _run_core_update() -> dict:
2829
2843
  import subprocess
2830
2844
  try:
2831
2845
  out = subprocess.run(
2832
- ["npx", "arkaos@latest", "update"],
2846
+ _win_shim(["npx", "arkaos@latest", "update"]),
2833
2847
  capture_output=True, text=True, timeout=600,
2834
2848
  )
2835
2849
  tail = ((out.stdout or "") + (out.stderr or ""))[-2000:]
@@ -2994,30 +3008,49 @@ async def ws_terminal(ws: WebSocket, session_id: str, token: str = Query("")):
2994
3008
  # left it. Sent before the live reader is attached, so the historical
2995
3009
  # prefix always precedes any new output (no interleave, no dup — these
2996
3010
  # bytes were already consumed from the kernel buffer when first read).
2997
- replay = session.scrollback()
2998
- if replay:
2999
- try:
3000
- await ws.send_bytes(replay)
3001
- except Exception:
3002
- await ws.close(code=1011, reason="replay failed")
3003
- return
3004
-
3005
3011
  loop = asyncio.get_event_loop()
3006
3012
  output_queue: asyncio.Queue = asyncio.Queue()
3007
3013
 
3008
- def _on_readable():
3014
+ # POSIX exposes a pollable master fd (add_reader); the Windows ConPTY
3015
+ # backend has none (master_fd == -1) and instead pushes output from its
3016
+ # own reader thread into a registered listener.
3017
+ use_fd_reader = getattr(session, "master_fd", -1) >= 0
3018
+
3019
+ if use_fd_reader:
3020
+ replay = session.scrollback()
3021
+ if replay:
3022
+ try:
3023
+ await ws.send_bytes(replay)
3024
+ except Exception:
3025
+ await ws.close(code=1011, reason="replay failed")
3026
+ return
3027
+
3028
+ def _on_readable():
3029
+ try:
3030
+ data = session.read(8192)
3031
+ except OSError:
3032
+ data = b""
3033
+ if data:
3034
+ output_queue.put_nowait(data)
3035
+
3009
3036
  try:
3010
- data = session.read(8192)
3011
- except OSError:
3012
- data = b""
3013
- if data:
3014
- output_queue.put_nowait(data)
3037
+ loop.add_reader(session.master_fd, _on_readable)
3038
+ except (ValueError, OSError):
3039
+ await ws.close(code=1011, reason="pty unavailable")
3040
+ return
3041
+ else:
3042
+ def _emit(data: bytes):
3043
+ loop.call_soon_threadsafe(output_queue.put_nowait, data)
3015
3044
 
3016
- try:
3017
- loop.add_reader(session.master_fd, _on_readable)
3018
- except (ValueError, OSError):
3019
- await ws.close(code=1011, reason="pty unavailable")
3020
- return
3045
+ # Atomic: snapshot scrollback and start the live feed with no gap.
3046
+ replay = session.attach(_emit)
3047
+ if replay:
3048
+ try:
3049
+ await ws.send_bytes(replay)
3050
+ except Exception:
3051
+ session.set_listener(None)
3052
+ await ws.close(code=1011, reason="replay failed")
3053
+ return
3021
3054
 
3022
3055
  async def pump_to_client():
3023
3056
  while True:
@@ -3060,13 +3093,16 @@ async def ws_terminal(ws: WebSocket, session_id: str, token: str = Query("")):
3060
3093
  pass
3061
3094
  finally:
3062
3095
  pump_task.cancel()
3063
- # Only the still-active connection owns the fd reader — a superseded
3064
- # connection tearing down must not remove its replacement's reader.
3096
+ # Only the still-active connection owns the reader — a superseded
3097
+ # connection tearing down must not detach its replacement's reader.
3065
3098
  if _terminal_conns.release(session_id, ws):
3066
- try:
3067
- loop.remove_reader(session.master_fd)
3068
- except (ValueError, OSError):
3069
- pass
3099
+ if use_fd_reader:
3100
+ try:
3101
+ loop.remove_reader(session.master_fd)
3102
+ except (ValueError, OSError):
3103
+ pass
3104
+ else:
3105
+ session.set_listener(None)
3070
3106
 
3071
3107
 
3072
3108
  @app.on_event("startup")
@@ -4233,7 +4269,7 @@ def keys_delete(key_name: str):
4233
4269
 
4234
4270
  @app.get("/api/metrics")
4235
4271
  def metrics():
4236
- metrics_file = Path("/tmp/arkaos-context-cache/hook-metrics.jsonl")
4272
+ metrics_file = arkaos_temp_dir("arkaos-context-cache", "hook-metrics.jsonl")
4237
4273
  if not metrics_file.exists():
4238
4274
  return {"entries": [], "avg_ms": 0}
4239
4275
  entries = []
@@ -14,6 +14,12 @@ import os
14
14
  import sys
15
15
  from pathlib import Path
16
16
 
17
+ # Windows consoles default to cp1252; progress lines with non-ASCII vault
18
+ # names would raise UnicodeEncodeError and kill the index run silently.
19
+ if hasattr(sys.stdout, "reconfigure"):
20
+ sys.stdout.reconfigure(encoding="utf-8")
21
+ sys.stderr.reconfigure(encoding="utf-8")
22
+
17
23
  ARKAOS_ROOT = Path(os.environ.get("ARKAOS_ROOT", Path(__file__).resolve().parent.parent))
18
24
  sys.path.insert(0, str(ARKAOS_ROOT))
19
25
 
@@ -77,13 +83,40 @@ def main() -> int:
77
83
  # Index mode
78
84
  directory = args.vault or args.dir
79
85
  if not directory:
80
- # Auto-detect vault from config
86
+ # Auto-detect vault from config. vault_path may be a template like
87
+ # "${VAULT_PATH}" — resolve it through the canonical path resolver
88
+ # (ARKAOS_VAULT_PATH env / profile.json vaultPath) before testing.
81
89
  config_path = ARKAOS_ROOT / "knowledge" / "obsidian-config.json"
82
90
  if config_path.exists():
83
- config = json.loads(config_path.read_text())
91
+ config = json.loads(config_path.read_text(encoding="utf-8"))
84
92
  vault = config.get("vault_path", "")
93
+ if vault and "${" in vault:
94
+ try:
95
+ from core.runtime.path_resolver import resolve
96
+
97
+ vault = resolve(vault)
98
+ except Exception:
99
+ vault = ""
100
+ if vault and Path(vault).exists():
101
+ directory = vault
102
+ if not args.json_output:
103
+ print(f"Vault from config: {directory}", file=sys.stderr)
104
+
105
+ if not directory:
106
+ # profile.json vaultPath — set by `npx arkaos install` and the
107
+ # authoritative answer to "where is the user's vault".
108
+ profile_path = Path.home() / ".arkaos" / "profile.json"
109
+ if profile_path.exists():
110
+ try:
111
+ vault = json.loads(
112
+ profile_path.read_text(encoding="utf-8")
113
+ ).get("vaultPath", "")
114
+ except (json.JSONDecodeError, OSError):
115
+ vault = ""
85
116
  if vault and Path(vault).exists():
86
117
  directory = vault
118
+ if not args.json_output:
119
+ print(f"Vault from profile: {directory}", file=sys.stderr)
87
120
 
88
121
  if not directory:
89
122
  # Try common vault locations
@@ -30,6 +30,7 @@ $arkaosHome = Join-Path $env:USERPROFILE '.arkaos'
30
30
  $pidFile = Join-Path $arkaosHome 'dashboard.pid'
31
31
  $portFile = Join-Path $arkaosHome 'dashboard.ports'
32
32
  $apiLog = Join-Path $arkaosHome 'api.log'
33
+ $apiErrLog = Join-Path $arkaosHome 'api.err.log'
33
34
 
34
35
  $null = New-Item -ItemType Directory -Force -Path $arkaosHome -ErrorAction SilentlyContinue
35
36
 
@@ -132,7 +133,7 @@ try {
132
133
  -ArgumentList @($dashboardApi, '--port', "$apiPort") `
133
134
  -WorkingDirectory $arkaosRoot `
134
135
  -RedirectStandardOutput $apiLog `
135
- -RedirectStandardError $apiLog `
136
+ -RedirectStandardError $apiErrLog `
136
137
  -NoNewWindow `
137
138
  -PassThru
138
139
  } finally {
@@ -160,10 +161,10 @@ for ($i = 0; $i -lt 20; $i++) {
160
161
  if ($apiReady) {
161
162
  Write-Host " API: http://localhost:$apiPort" -ForegroundColor Green
162
163
  } else {
163
- Write-Host " ! API may still be starting (check log: $apiLog)" -ForegroundColor Yellow
164
- if (Test-Path -LiteralPath $apiLog) {
164
+ Write-Host " ! API may still be starting (check log: $apiErrLog)" -ForegroundColor Yellow
165
+ if (Test-Path -LiteralPath $apiErrLog) {
165
166
  try {
166
- $lastError = (Get-Content -LiteralPath $apiLog -Tail 3 -ErrorAction SilentlyContinue | Select-Object -First 1)
167
+ $lastError = (Get-Content -LiteralPath $apiErrLog -Tail 3 -ErrorAction SilentlyContinue | Select-Object -First 1)
167
168
  if ($lastError) {
168
169
  Write-Host " Last error: $lastError" -ForegroundColor DarkGray
169
170
  }
@@ -180,11 +181,23 @@ $nuxtServer = Join-Path $nuxtOutput 'server\index.mjs'
180
181
 
181
182
  function Start-NuxtDev {
182
183
  param([int]$Port, [int]$ApiPort, [string]$WorkingDir)
183
- # `npx nuxt dev --port <n>` works from cmd.exe; PowerShell resolves npx
184
- # via the npx.cmd shim installed with Node.js.
184
+ # Launch node directly against the Nuxt CLI entry. The 'npx' shim is a
185
+ # .cmd on Windows, which Start-Process -NoNewWindow cannot exec ("%1 is
186
+ # not a valid Win32 application"), and wrapping it in cmd.exe yields a
187
+ # process tree that dies with the launcher. A direct node.exe child
188
+ # detaches cleanly and survives, like the API process.
185
189
  $savedApi = $env:NUXT_PUBLIC_API_BASE
186
190
  $env:NUXT_PUBLIC_API_BASE = "http://localhost:$ApiPort"
187
191
  try {
192
+ $nuxtBin = Join-Path $WorkingDir 'node_modules\nuxt\bin\nuxt.mjs'
193
+ if (Test-Path -LiteralPath $nuxtBin) {
194
+ return Start-Process -FilePath 'node' `
195
+ -ArgumentList @($nuxtBin,'dev','--port',"$Port") `
196
+ -WorkingDirectory $WorkingDir `
197
+ -NoNewWindow `
198
+ -PassThru
199
+ }
200
+ # Fallback (non-Windows or missing local bin): the npx shim is fine.
188
201
  return Start-Process -FilePath 'npx' `
189
202
  -ArgumentList @('nuxt','dev','--port',"$Port") `
190
203
  -WorkingDirectory $WorkingDir `
@@ -215,18 +228,25 @@ if (Test-Path -LiteralPath $nuxtServer) {
215
228
  Write-Host " Starting UI (dev) on :$uiPort..."
216
229
  $uiProc = Start-NuxtDev -Port $uiPort -ApiPort $apiPort -WorkingDir $dashboardDir
217
230
  } else {
218
- # Auto-install and start
219
- Write-Host ' Installing dashboard dependencies...'
220
- $installer = if (Get-Command pnpm -ErrorAction SilentlyContinue) { 'pnpm' } else { 'npm' }
231
+ # Auto-install and start. The dashboard pins pnpm (package.json
232
+ # packageManager) — npm's flat node_modules layout breaks Nuxt 4's
233
+ # server-entry resolution ("No entry found in rollupOptions.input"), so
234
+ # install with the pinned pnpm via corepack (always shipped with Node).
235
+ # corepack/pnpm are .cmd shims that Start-Process -NoNewWindow cannot
236
+ # exec directly on Windows, so route through cmd.exe.
237
+ Write-Host ' Installing dashboard dependencies (pnpm via corepack)...'
238
+ $savedPrompt = $env:COREPACK_ENABLE_DOWNLOAD_PROMPT
239
+ $env:COREPACK_ENABLE_DOWNLOAD_PROMPT = '0'
221
240
  try {
222
- Start-Process -FilePath $installer `
223
- -ArgumentList @('install','--silent') `
241
+ Start-Process -FilePath $env:ComSpec `
242
+ -ArgumentList @('/c','corepack','pnpm','install') `
224
243
  -WorkingDirectory $dashboardDir `
225
244
  -NoNewWindow `
226
- -Wait `
227
- -PassThru | Out-Null
245
+ -Wait | Out-Null
228
246
  } catch {
229
- Write-Host " ! Dashboard install failed ($installer). API-only mode." -ForegroundColor Yellow
247
+ Write-Host " ! Dashboard install failed. API-only mode." -ForegroundColor Yellow
248
+ } finally {
249
+ $env:COREPACK_ENABLE_DOWNLOAD_PROMPT = $savedPrompt
230
250
  }
231
251
  if (Test-Path -LiteralPath $nuxtNodeModules) {
232
252
  Write-Host " Starting UI (dev) on :$uiPort..."
@@ -24,12 +24,14 @@ from typing import Any
24
24
  ARKAOS_ROOT = Path(os.environ.get("ARKAOS_ROOT", Path(__file__).resolve().parent.parent))
25
25
  sys.path.insert(0, str(ARKAOS_ROOT))
26
26
 
27
+ from core.shared.temp_paths import arkaos_temp_dir # noqa: E402
28
+
27
29
  # Layer tags carrying the actually-injected KB counts (ground truth for
28
30
  # reconciling the model's self-reported `[arka:meta] kb=N`). The optional
29
31
  # ` degraded=keyword` suffix (RAG honesty, PR-3 v4.1) must still count.
30
32
  _KB_CONTEXT_TAG_RE = re.compile(r"\[kb-context:(\d+)[^\]]*\]")
31
33
  _KNOWLEDGE_TAG_RE = re.compile(r"\[knowledge:(\d+) chunks?[^\]]*\]")
32
- _KB_INJECTED_DIR = Path("/tmp/arkaos-kb-injected")
34
+ _KB_INJECTED_DIR = arkaos_temp_dir("arkaos-kb-injected")
33
35
 
34
36
 
35
37
  def _count_injected_kb(result: Any) -> tuple[int, int]: