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
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.3.6
1
+ 4.4.0
package/bin/arka-py CHANGED
@@ -30,14 +30,29 @@ done
30
30
  : "${ARKA_PY:=python3}"
31
31
 
32
32
  # ─── Make `-m core.*` resolvable regardless of cwd ────────────────────────
33
- # ARKAOS_ROOT (env) wins; else the installed repo pointer; else the repo this
34
- # shim lives in (dev checkout: bin/ -> repo root).
33
+ # ARKAOS_ROOT (env) wins unconditionally an explicit operator override is
34
+ # honored even if wrong, so misconfiguration stays loud and debuggable.
35
+ # The guessed candidates are validated before winning: `.repo-path` points
36
+ # at an npx cache that `npm cache clean` can purge, and ~/.arkaos/core is
37
+ # the cognitive scheduler's minimal copy (no core/sync). A candidate only
38
+ # counts when it holds the full package (core/sync/__init__.py marker).
39
+ # Chain: .repo-path → dev checkout (bin/ -> repo root) → ~/.arkaos/lib
40
+ # (stable snapshot written by the installer, survives cache purges).
35
41
  _root="${ARKAOS_ROOT:-}"
36
- if [ -z "$_root" ] && [ -f "$HOME/.arkaos/.repo-path" ]; then
37
- _root="$(cat "$HOME/.arkaos/.repo-path" 2>/dev/null || true)"
38
- fi
39
- if [ -z "$_root" ] && [ -f "$_self/../core/hooks/__init__.py" ]; then
40
- _root="$(cd "$_self/.." && pwd)"
42
+ if [ -z "$_root" ]; then
43
+ _repo_ptr=""
44
+ if [ -f "$HOME/.arkaos/.repo-path" ]; then
45
+ _repo_ptr="$(cat "$HOME/.arkaos/.repo-path" 2>/dev/null || true)"
46
+ fi
47
+ for _cand in \
48
+ "$_repo_ptr" \
49
+ "$_self/.." \
50
+ "$HOME/.arkaos/lib"; do
51
+ if [ -n "$_cand" ] && [ -f "$_cand/core/sync/__init__.py" ]; then
52
+ _root="$(cd "$_cand" && pwd)"
53
+ break
54
+ fi
55
+ done
41
56
  fi
42
57
  if [ -n "$_root" ] && [ -d "$_root" ]; then
43
58
  export PYTHONPATH="${_root}${PYTHONPATH:+:$PYTHONPATH}"
@@ -40,4 +40,35 @@ function Resolve-ArkaPython {
40
40
  return "python"
41
41
  }
42
42
 
43
+ # Windows mirror of arka_resolve_root() in arka_python.sh: env override wins
44
+ # unconditionally; guessed candidates must contain core/sync/__init__.py
45
+ # (the full-package marker — `.repo-path` points at an npx cache that
46
+ # `npm cache clean` can purge; ~/.arkaos/lib is the installer's snapshot).
47
+ function Resolve-ArkaRoot {
48
+ if (-not [string]::IsNullOrWhiteSpace($env:ARKAOS_ROOT)) {
49
+ return $env:ARKAOS_ROOT
50
+ }
51
+ $base = if ($env:USERPROFILE) { $env:USERPROFILE } elseif ($HOME) { $HOME } else { "" }
52
+ $repo = ""
53
+ $repoPathFile = Join-Path $base ".arkaos\.repo-path"
54
+ if ($base -and (Test-Path -LiteralPath $repoPathFile)) {
55
+ $repo = (Get-Content $repoPathFile -Raw).Trim()
56
+ }
57
+ if ($repo -and (Test-Path -LiteralPath (Join-Path $repo "core\sync\__init__.py"))) {
58
+ return $repo
59
+ }
60
+ $lib = Join-Path $base ".arkaos\lib"
61
+ if ($base -and (Test-Path -LiteralPath (Join-Path $lib "core\sync\__init__.py"))) {
62
+ return $lib
63
+ }
64
+ if ($repo -and (Test-Path -LiteralPath $repo)) {
65
+ return $repo
66
+ }
67
+ if ($base -and (Test-Path -LiteralPath (Join-Path $base ".arkaos"))) {
68
+ return (Join-Path $base ".arkaos")
69
+ }
70
+ if ($env:ARKA_OS) { return $env:ARKA_OS }
71
+ return (Join-Path $base ".claude\skills\arkaos")
72
+ }
73
+
43
74
  $env:ARKA_PY = Resolve-ArkaPython
@@ -61,3 +61,41 @@ arka_resolve_python() {
61
61
  # keep the export unconditional.
62
62
  ARKA_PY="$(arka_resolve_python)" || true
63
63
  export ARKA_PY
64
+
65
+ # Contract of arka_resolve_root():
66
+ # Echoes the ArkaOS root for `-m core.*` execution. env ARKAOS_ROOT wins
67
+ # unconditionally (explicit operator override stays loud and debuggable).
68
+ # Guessed candidates are validated on core/sync/__init__.py — the marker
69
+ # that distinguishes the full package from the cognitive scheduler's
70
+ # partial ~/.arkaos/core copy — because `.repo-path` points at an npx
71
+ # cache that `npm cache clean` can purge at any time. Chain:
72
+ # .repo-path (validated) → ~/.arkaos/lib snapshot (validated, written by
73
+ # the installer) → .repo-path even without core (legacy VERSION readers)
74
+ # → ~/.arkaos → ARKA_OS env → ~/.claude/skills/arkaos.
75
+ arka_resolve_root() {
76
+ if [ -n "${ARKAOS_ROOT:-}" ]; then
77
+ printf '%s\n' "$ARKAOS_ROOT"
78
+ return 0
79
+ fi
80
+ local repo=""
81
+ if [ -f "$HOME/.arkaos/.repo-path" ]; then
82
+ repo="$(cat "$HOME/.arkaos/.repo-path" 2>/dev/null || true)"
83
+ fi
84
+ if [ -n "$repo" ] && [ -f "$repo/core/sync/__init__.py" ]; then
85
+ printf '%s\n' "$repo"
86
+ return 0
87
+ fi
88
+ if [ -f "$HOME/.arkaos/lib/core/sync/__init__.py" ]; then
89
+ printf '%s\n' "$HOME/.arkaos/lib"
90
+ return 0
91
+ fi
92
+ if [ -n "$repo" ] && [ -d "$repo" ]; then
93
+ printf '%s\n' "$repo"
94
+ return 0
95
+ fi
96
+ if [ -d "$HOME/.arkaos" ]; then
97
+ printf '%s\n' "$HOME/.arkaos"
98
+ return 0
99
+ fi
100
+ printf '%s\n' "${ARKA_OS:-$HOME/.claude/skills/arkaos}"
101
+ }
@@ -150,6 +150,14 @@ if ($shouldProcessGotchas -and $null -ne $payload) {
150
150
  $arkaosRootPtu = (Get-Content -Raw -LiteralPath $repoPathFile -Encoding UTF8).Trim()
151
151
  } catch { }
152
152
  }
153
+ # .repo-path may point at a purged npx cache — fall through
154
+ # to the ~/.arkaos/lib snapshot when core is missing.
155
+ if (-not ($arkaosRootPtu -and (Test-Path -LiteralPath (Join-Path $arkaosRootPtu 'core\sync\__init__.py')))) {
156
+ $libPtu = Join-Path $env:USERPROFILE '.arkaos\lib'
157
+ if (Test-Path -LiteralPath (Join-Path $libPtu 'core\sync\__init__.py')) {
158
+ $arkaosRootPtu = $libPtu
159
+ }
160
+ }
153
161
  }
154
162
  if (-not $arkaosRootPtu) {
155
163
  $arkaosRootPtu = Join-Path $env:USERPROFILE '.arkaos'
@@ -20,7 +20,17 @@
20
20
  # - Hook metrics. Output: JSON to stdout. Timeout: 5s.
21
21
  # ============================================================================
22
22
 
23
- if [ -z "${ARKAOS_ROOT:-}" ]; then
23
+ # ─── Shared Python resolver (exports ARKA_PY) ──────────────────────────
24
+ _ARKA_LIB="$(dirname "${BASH_SOURCE[0]:-$0}")/_lib/arka_python.sh"
25
+ if [ -f "$_ARKA_LIB" ]; then . "$_ARKA_LIB"; else ARKA_PY="python3"; fi
26
+
27
+ # ─── Resolve ARKAOS_ROOT (validated — see arka_resolve_root in _lib) ────
28
+ # .repo-path can point at a purged npx cache; the shared resolver falls
29
+ # through to the ~/.arkaos/lib snapshot instead of exporting a dead root.
30
+ if command -v arka_resolve_root >/dev/null 2>&1; then
31
+ ARKAOS_ROOT="$(arka_resolve_root)"
32
+ elif [ -z "${ARKAOS_ROOT:-}" ]; then
33
+ # Legacy chain (pre-snapshot _lib deployment)
24
34
  if [ -f "$HOME/.arkaos/.repo-path" ]; then
25
35
  ARKAOS_ROOT=$(cat "$HOME/.arkaos/.repo-path")
26
36
  elif [ -d "$HOME/.arkaos" ]; then
@@ -31,10 +41,6 @@ if [ -z "${ARKAOS_ROOT:-}" ]; then
31
41
  fi
32
42
  export ARKAOS_ROOT
33
43
 
34
- # ─── Shared Python resolver (exports ARKA_PY) ──────────────────────────
35
- _ARKA_LIB="$(dirname "${BASH_SOURCE[0]:-$0}")/_lib/arka_python.sh"
36
- if [ -f "$_ARKA_LIB" ]; then . "$_ARKA_LIB"; else ARKA_PY="python3"; fi
37
-
38
44
  if ! command -v "$ARKA_PY" >/dev/null 2>&1; then
39
45
  echo '{}'
40
46
  exit 0
@@ -27,8 +27,17 @@ $transcriptPath = [string]$inp.transcript_path
27
27
  $sessionId = [string]$inp.session_id
28
28
  $cwd = [string]$inp.cwd
29
29
 
30
- # --- Resolve ARKAOS_ROOT ---
31
- if ([string]::IsNullOrWhiteSpace($env:ARKAOS_ROOT)) {
30
+ # Shared resolver (venv-first, yaml-verified). Mirrors _lib/arka_python.sh.
31
+ $arkaLib = Join-Path $PSScriptRoot "_lib\arka_python.ps1"
32
+ if (Test-Path -LiteralPath $arkaLib) { . $arkaLib }
33
+
34
+ # --- Resolve ARKAOS_ROOT (validated — see Resolve-ArkaRoot in _lib) ---
35
+ # .repo-path can point at a purged npx cache; the shared resolver falls
36
+ # through to the ~/.arkaos/lib snapshot instead of exporting a dead root.
37
+ if (Get-Command Resolve-ArkaRoot -ErrorAction SilentlyContinue) {
38
+ $env:ARKAOS_ROOT = Resolve-ArkaRoot
39
+ } elseif ([string]::IsNullOrWhiteSpace($env:ARKAOS_ROOT)) {
40
+ # Legacy chain (pre-snapshot _lib deployment)
32
41
  $repoPathFile = Join-Path $HOME ".arkaos/.repo-path"
33
42
  if (Test-Path $repoPathFile) {
34
43
  $env:ARKAOS_ROOT = (Get-Content $repoPathFile -Raw).Trim()
@@ -38,10 +47,6 @@ if ([string]::IsNullOrWhiteSpace($env:ARKAOS_ROOT)) {
38
47
  $env:ARKAOS_ROOT = if ($env:ARKA_OS) { $env:ARKA_OS } else { Join-Path $HOME ".claude/skills/arkaos" }
39
48
  }
40
49
  }
41
-
42
- # Shared resolver (venv-first, yaml-verified). Mirrors _lib/arka_python.sh.
43
- $arkaLib = Join-Path $PSScriptRoot "_lib\arka_python.ps1"
44
- if (Test-Path -LiteralPath $arkaLib) { . $arkaLib }
45
50
  $pythonExe = $env:ARKA_PY
46
51
  if (-not $pythonExe) { exit 0 }
47
52
 
@@ -22,8 +22,17 @@
22
22
  # flags are honored inside the python modules. Timeout: 10s.
23
23
  # ============================================================================
24
24
 
25
- # ─── Resolve ARKAOS_ROOT (env → .repo-path → ~/.arkaos → portable) ──────
26
- if [ -z "${ARKAOS_ROOT:-}" ]; then
25
+ # ─── Shared Python resolver (exports ARKA_PY) ──────────────────────────
26
+ _ARKA_LIB="$(dirname "${BASH_SOURCE[0]:-$0}")/_lib/arka_python.sh"
27
+ if [ -f "$_ARKA_LIB" ]; then . "$_ARKA_LIB"; else ARKA_PY="python3"; fi
28
+
29
+ # ─── Resolve ARKAOS_ROOT (validated — see arka_resolve_root in _lib) ────
30
+ # .repo-path can point at a purged npx cache; the shared resolver falls
31
+ # through to the ~/.arkaos/lib snapshot instead of exporting a dead root.
32
+ if command -v arka_resolve_root >/dev/null 2>&1; then
33
+ ARKAOS_ROOT="$(arka_resolve_root)"
34
+ elif [ -z "${ARKAOS_ROOT:-}" ]; then
35
+ # Legacy chain (pre-snapshot _lib deployment)
27
36
  if [ -f "$HOME/.arkaos/.repo-path" ]; then
28
37
  ARKAOS_ROOT=$(cat "$HOME/.arkaos/.repo-path")
29
38
  elif [ -d "$HOME/.arkaos" ]; then
@@ -34,10 +43,6 @@ if [ -z "${ARKAOS_ROOT:-}" ]; then
34
43
  fi
35
44
  export ARKAOS_ROOT
36
45
 
37
- # ─── Shared Python resolver (exports ARKA_PY) ──────────────────────────
38
- _ARKA_LIB="$(dirname "${BASH_SOURCE[0]:-$0}")/_lib/arka_python.sh"
39
- if [ -f "$_ARKA_LIB" ]; then . "$_ARKA_LIB"; else ARKA_PY="python3"; fi
40
-
41
46
  # ─── Degrade gracefully (fail open, same as before) ─────────────────────
42
47
  if ! command -v "$ARKA_PY" >/dev/null 2>&1; then
43
48
  exit 0
@@ -46,6 +46,16 @@ if (Test-Path -LiteralPath $repoPathFile) {
46
46
  }
47
47
  } catch { }
48
48
  }
49
+ # .repo-path may point at a purged npx cache — the installer's snapshot
50
+ # keeps the drift banner alive ('2.x' is the untouched default above).
51
+ if ($version -eq '2.x') {
52
+ $libVersionFile = Join-Path $arkaosHome 'lib\VERSION'
53
+ if (Test-Path -LiteralPath $libVersionFile) {
54
+ try {
55
+ $version = (Get-Content -Raw -LiteralPath $libVersionFile -Encoding UTF8).Trim()
56
+ } catch { }
57
+ }
58
+ }
49
59
 
50
60
  # ─── Time greeting ─────────────────────────────────────────────────────
51
61
  $hour = [int](Get-Date -Format 'HH')
@@ -22,6 +22,11 @@ if [ -f "$HOME/.arkaos/.repo-path" ]; then
22
22
  REPO=$(cat "$HOME/.arkaos/.repo-path")
23
23
  [ -f "$REPO/VERSION" ] && VERSION=$(cat "$REPO/VERSION" | tr -d '[:space:]')
24
24
  fi
25
+ # .repo-path may point at a purged npx cache — the installer's snapshot
26
+ # keeps the drift banner alive ("2.x" is the untouched default above).
27
+ if [ "$VERSION" = "2.x" ] && [ -f "$HOME/.arkaos/lib/VERSION" ]; then
28
+ VERSION=$(tr -d '[:space:]' < "$HOME/.arkaos/lib/VERSION")
29
+ fi
25
30
 
26
31
  # ─── Static greeting (cache-friendly) ──────────────────────────────────
27
32
  # Time-of-day branching removed: it invalidated prompt cache 3x/day without
@@ -29,7 +29,15 @@ if ([string]::IsNullOrWhiteSpace($sessionId) -or -not (Test-Path $wfMarker)) {
29
29
  exit 0
30
30
  }
31
31
 
32
- if ([string]::IsNullOrWhiteSpace($env:ARKAOS_ROOT)) {
32
+ # Resolve ARKAOS_ROOT via the validated shared resolver: .repo-path can
33
+ # point at a purged npx cache; fall through to the ~/.arkaos/lib snapshot
34
+ # instead of exporting a dead root.
35
+ $arkaLibEarly = Join-Path $PSScriptRoot "_lib\arka_python.ps1"
36
+ if (Test-Path -LiteralPath $arkaLibEarly) { . $arkaLibEarly }
37
+ if (Get-Command Resolve-ArkaRoot -ErrorAction SilentlyContinue) {
38
+ $env:ARKAOS_ROOT = Resolve-ArkaRoot
39
+ } elseif ([string]::IsNullOrWhiteSpace($env:ARKAOS_ROOT)) {
40
+ # Legacy chain (pre-snapshot _lib deployment)
33
41
  $repoPathFile = Join-Path $HOME ".arkaos/.repo-path"
34
42
  if (Test-Path $repoPathFile) {
35
43
  $env:ARKAOS_ROOT = (Get-Content $repoPathFile -Raw).Trim()
@@ -22,7 +22,17 @@
22
22
  # Timeout: 5s | Always exit 0.
23
23
  # ============================================================================
24
24
 
25
- if [ -z "${ARKAOS_ROOT:-}" ]; then
25
+ # ─── Shared Python resolver (exports ARKA_PY) ──────────────────────────
26
+ _ARKA_LIB="$(dirname "${BASH_SOURCE[0]:-$0}")/_lib/arka_python.sh"
27
+ if [ -f "$_ARKA_LIB" ]; then . "$_ARKA_LIB"; else ARKA_PY="python3"; fi
28
+
29
+ # ─── Resolve ARKAOS_ROOT (validated — see arka_resolve_root in _lib) ────
30
+ # .repo-path can point at a purged npx cache; the shared resolver falls
31
+ # through to the ~/.arkaos/lib snapshot instead of exporting a dead root.
32
+ if command -v arka_resolve_root >/dev/null 2>&1; then
33
+ ARKAOS_ROOT="$(arka_resolve_root)"
34
+ elif [ -z "${ARKAOS_ROOT:-}" ]; then
35
+ # Legacy chain (pre-snapshot _lib deployment)
26
36
  if [ -f "$HOME/.arkaos/.repo-path" ]; then
27
37
  ARKAOS_ROOT=$(cat "$HOME/.arkaos/.repo-path")
28
38
  elif [ -d "$HOME/.arkaos" ]; then
@@ -33,10 +43,6 @@ if [ -z "${ARKAOS_ROOT:-}" ]; then
33
43
  fi
34
44
  export ARKAOS_ROOT
35
45
 
36
- # ─── Shared Python resolver (exports ARKA_PY) ──────────────────────────
37
- _ARKA_LIB="$(dirname "${BASH_SOURCE[0]:-$0}")/_lib/arka_python.sh"
38
- if [ -f "$_ARKA_LIB" ]; then . "$_ARKA_LIB"; else ARKA_PY="python3"; fi
39
-
40
46
  if ! command -v "$ARKA_PY" >/dev/null 2>&1; then
41
47
  exit 0
42
48
  fi
@@ -80,6 +80,14 @@ try {
80
80
  $arkaosRootUps = (Get-Content -Raw -LiteralPath $repoPathFileUps -Encoding UTF8).Trim()
81
81
  } catch { }
82
82
  }
83
+ # .repo-path may point at a purged npx cache — fall through to
84
+ # the ~/.arkaos/lib snapshot when the core package is missing.
85
+ if (-not ($arkaosRootUps -and (Test-Path -LiteralPath (Join-Path $arkaosRootUps 'core\sync\__init__.py')))) {
86
+ $libUps = Join-Path $env:USERPROFILE '.arkaos\lib'
87
+ if (Test-Path -LiteralPath (Join-Path $libUps 'core\sync\__init__.py')) {
88
+ $arkaosRootUps = $libUps
89
+ }
90
+ }
83
91
  }
84
92
  if (-not $arkaosRootUps) {
85
93
  $arkaosRootUps = Join-Path $env:USERPROFILE '.arkaos'
@@ -160,6 +168,16 @@ if ($env:ARKAOS_ROOT) {
160
168
  $arkaosRoot = $null
161
169
  }
162
170
  }
171
+ # .repo-path may point at a purged npx cache — prefer the ~/.arkaos/lib
172
+ # snapshot over a root without the core package (env override untouched).
173
+ if (-not $env:ARKAOS_ROOT) {
174
+ if (-not ($arkaosRoot -and (Test-Path -LiteralPath (Join-Path $arkaosRoot 'core\sync\__init__.py')))) {
175
+ $libRoot = Join-Path $arkaosHome 'lib'
176
+ if (Test-Path -LiteralPath (Join-Path $libRoot 'core\sync\__init__.py')) {
177
+ $arkaosRoot = $libRoot
178
+ }
179
+ }
180
+ }
163
181
  if (-not $arkaosRoot) {
164
182
  if (Test-Path -LiteralPath $arkaosHome -PathType Container) {
165
183
  $arkaosRoot = $arkaosHome
@@ -23,7 +23,17 @@
23
23
  # Timeout: 10s | Output: JSON to stdout | Target: <100ms
24
24
  # ============================================================================
25
25
 
26
- if [ -z "${ARKAOS_ROOT:-}" ]; then
26
+ # ─── Shared Python resolver (exports ARKA_PY) ──────────────────────────
27
+ _ARKA_LIB="$(dirname "${BASH_SOURCE[0]:-$0}")/_lib/arka_python.sh"
28
+ if [ -f "$_ARKA_LIB" ]; then . "$_ARKA_LIB"; else ARKA_PY="python3"; fi
29
+
30
+ # ─── Resolve ARKAOS_ROOT (validated — see arka_resolve_root in _lib) ────
31
+ # .repo-path can point at a purged npx cache; the shared resolver falls
32
+ # through to the ~/.arkaos/lib snapshot instead of exporting a dead root.
33
+ if command -v arka_resolve_root >/dev/null 2>&1; then
34
+ ARKAOS_ROOT="$(arka_resolve_root)"
35
+ elif [ -z "${ARKAOS_ROOT:-}" ]; then
36
+ # Legacy chain (pre-snapshot _lib deployment)
27
37
  if [ -f "$HOME/.arkaos/.repo-path" ]; then
28
38
  ARKAOS_ROOT=$(cat "$HOME/.arkaos/.repo-path")
29
39
  elif [ -d "$HOME/.arkaos" ]; then
@@ -34,10 +44,6 @@ if [ -z "${ARKAOS_ROOT:-}" ]; then
34
44
  fi
35
45
  export ARKAOS_ROOT
36
46
 
37
- # ─── Shared Python resolver (exports ARKA_PY) ──────────────────────────
38
- _ARKA_LIB="$(dirname "${BASH_SOURCE[0]:-$0}")/_lib/arka_python.sh"
39
- if [ -f "$_ARKA_LIB" ]; then . "$_ARKA_LIB"; else ARKA_PY="python3"; fi
40
-
41
47
  _ARKA_L0_FALLBACK='{"additionalContext": "[Constitution] NON-NEGOTIABLE: branch-isolation, obsidian-output, authority-boundaries, security-gate, context-first, solid-clean-code, spec-driven, human-writing, squad-routing, full-visibility, sequential-validation, mandatory-qa, arka-supremacy | QUALITY-GATE: marta-cqo, eduardo-copy, francisca-tech-ux | MUST: conventional-commits, test-coverage, pattern-matching, actionable-output, memory-persistence"}'
42
48
 
43
49
  if ! command -v "$ARKA_PY" >/dev/null 2>&1; then
@@ -275,11 +275,15 @@ class ArkaScheduler:
275
275
  extra_paths = [
276
276
  os.path.join(home, ".local", "bin"),
277
277
  os.path.join(home, ".arkaos", "bin"),
278
- "/usr/local/bin",
279
278
  ]
279
+ if os.name != "nt":
280
+ extra_paths.append("/usr/local/bin")
280
281
  env = os.environ.copy()
281
- existing = env.get("PATH", "/usr/bin:/bin")
282
- env["PATH"] = ":".join(extra_paths) + ":" + existing
282
+ existing = env.get("PATH", "" if os.name == "nt" else "/usr/bin:/bin")
283
+ # os.pathsep is ';' on Windows (where drive-letter entries contain
284
+ # ':') and ':' on POSIX, so the joined PATH stays valid on both.
285
+ parts = extra_paths + ([existing] if existing else [])
286
+ env["PATH"] = os.pathsep.join(parts)
283
287
  return env
284
288
 
285
289
  def _run_attempt(
@@ -23,6 +23,7 @@ from enum import Enum
23
23
  from pathlib import Path
24
24
  from typing import Optional
25
25
 
26
+ from core.shared.temp_paths import arkaos_temp_dir
26
27
  from core.forge.schema import (
27
28
  ForgeContext,
28
29
  ForgePlan,
@@ -361,7 +362,7 @@ class ForgeOrchestrator:
361
362
  raise RuntimeError("No active forge plan.")
362
363
 
363
364
  html = render_html(self._current_plan)
364
- path = f"/tmp/forge-{self._current_plan.id}.html"
365
+ path = str(arkaos_temp_dir(f"forge-{self._current_plan.id}.html"))
365
366
  Path(path).write_text(html, encoding="utf-8")
366
367
  return path
367
368
 
@@ -51,21 +51,41 @@ def get_str(data: dict, *keys: str) -> str:
51
51
  return str(cur)
52
52
 
53
53
 
54
+ def _has_core_package(root: str) -> bool:
55
+ # core/sync/__init__.py distinguishes the full package from the
56
+ # cognitive scheduler's minimal core/ copy (cognition + workflow only).
57
+ try:
58
+ return (Path(root) / "core" / "sync" / "__init__.py").is_file()
59
+ except OSError:
60
+ return False
61
+
62
+
54
63
  def resolve_arkaos_root() -> str:
55
- """Resolve ARKAOS_ROOT with the same precedence as the bash hooks.
64
+ """Resolve ARKAOS_ROOT with the hook wrappers' validated chain.
56
65
 
57
- env ARKAOS_ROOT → ~/.arkaos/.repo-path → ~/.arkaos → ARKA_OS env →
58
- ~/.claude/skills/arkaos (portable fallback).
66
+ env ARKAOS_ROOT (unconditional operator override) → ~/.arkaos/.repo-path
67
+ (validated) → ~/.arkaos/lib stable snapshot (validated) .repo-path
68
+ even without the core package (legacy VERSION readers) → ~/.arkaos →
69
+ ARKA_OS env → ~/.claude/skills/arkaos (portable fallback).
70
+
71
+ Mirrors arka_resolve_root() in config/hooks/_lib/arka_python.sh;
72
+ bin/arka-py adds one extra step (its own dev checkout) between
73
+ .repo-path and the snapshot.
74
+
75
+ Validation matters because .repo-path points at an npx cache that
76
+ `npm cache clean` can purge at any time.
59
77
  """
60
78
  env_root = os.environ.get("ARKAOS_ROOT", "").strip()
61
79
  if env_root:
62
80
  return env_root
63
- repo_file = Path.home() / ".arkaos" / ".repo-path"
64
- if repo_file.is_file():
65
- try:
66
- return repo_file.read_text(encoding="utf-8").strip()
67
- except OSError:
68
- pass
81
+ repo = repo_path()
82
+ if repo and _has_core_package(repo):
83
+ return repo
84
+ lib = Path.home() / ".arkaos" / "lib"
85
+ if _has_core_package(str(lib)):
86
+ return str(lib)
87
+ if repo and Path(repo).is_dir():
88
+ return repo
69
89
  if (Path.home() / ".arkaos").is_dir():
70
90
  return str(Path.home() / ".arkaos")
71
91
  return os.environ.get(
@@ -30,6 +30,7 @@ import re
30
30
  from datetime import datetime, timezone
31
31
  from pathlib import Path
32
32
 
33
+ from core.shared.temp_paths import arkaos_temp_dir
33
34
  from core.hooks._shared import (
34
35
  ensure_root_on_path,
35
36
  get_str,
@@ -104,7 +105,7 @@ def _write_tmp_state(subdir: str, safe_sid: str, payload: dict) -> None:
104
105
  """Owner-only /tmp state file (umask 0o077 — PR25 v2.46.1)."""
105
106
  prev_umask = os.umask(0o077)
106
107
  try:
107
- state_dir = Path("/tmp") / subdir
108
+ state_dir = arkaos_temp_dir(subdir)
108
109
  state_dir.mkdir(parents=True, exist_ok=True)
109
110
  (state_dir / f"{safe_sid}.json").write_text(
110
111
  json.dumps(payload), encoding="utf-8"
@@ -220,7 +221,7 @@ def _flow_checks(
220
221
  kb_reported = parse_reported_kb(last)
221
222
  if safe_sid:
222
223
  injected_path = (
223
- Path("/tmp/arkaos-kb-injected") / f"{safe_sid}.json"
224
+ arkaos_temp_dir("arkaos-kb-injected") / f"{safe_sid}.json"
224
225
  )
225
226
  if injected_path.exists():
226
227
  injected_data = json.loads(
@@ -369,7 +370,7 @@ def main(stdin_json: dict | None = None) -> int:
369
370
  _native_usage(transcript_path, session_id, raw)
370
371
 
371
372
  # Only evaluate sessions where the classifier flagged creation intent.
372
- wf_marker = Path("/tmp/arkaos-wf-required") / session_id if session_id else None
373
+ wf_marker = arkaos_temp_dir("arkaos-wf-required") / session_id if session_id else None
373
374
  if wf_marker is None or not safe_session_id(session_id) or not wf_marker.is_file():
374
375
  return 0
375
376
 
@@ -36,6 +36,7 @@ import subprocess
36
36
  import time
37
37
  from pathlib import Path
38
38
 
39
+ from core.shared.temp_paths import arkaos_temp_dir
39
40
  from core.hooks._shared import (
40
41
  ensure_root_on_path,
41
42
  get_str,
@@ -45,7 +46,7 @@ from core.hooks._shared import (
45
46
  safe_session_id,
46
47
  )
47
48
 
48
- _CACHE_DIR = Path("/tmp/arkaos-context-cache")
49
+ _CACHE_DIR = arkaos_temp_dir("arkaos-context-cache")
49
50
  _CACHE_TTL = 300 # Constitution cache: 5 minutes
50
51
 
51
52
  _L0_FALLBACK = (
@@ -282,7 +283,7 @@ def _kb_auto_inject(root: str, user_input: str) -> str:
282
283
  project_hash = hashlib.md5(
283
284
  root.encode(), usedforsecurity=False
284
285
  ).hexdigest()[:12]
285
- cache_dir = Path("/tmp") / f"arkaos-kb-{project_hash}"
286
+ cache_dir = arkaos_temp_dir(f"arkaos-kb-{project_hash}")
286
287
  if not cache_dir.is_dir():
287
288
  return ""
288
289
  try:
@@ -439,7 +440,7 @@ def _wf_mark_required(session_id: str) -> None:
439
440
  if safe_session_id(session_id) is None:
440
441
  return
441
442
  marker_dir = Path(
442
- os.environ.get("ARKA_WF_REQUIRED_DIR", "/tmp/arkaos-wf-required")
443
+ os.environ.get("ARKA_WF_REQUIRED_DIR", str(arkaos_temp_dir("arkaos-wf-required")))
443
444
  )
444
445
  try:
445
446
  marker_dir.mkdir(parents=True, exist_ok=True)
@@ -469,7 +470,7 @@ def _cognitive_hits(session_id: str) -> str:
469
470
 
470
471
  def _one_shot_nudge(subdir: str, session_id: str) -> str:
471
472
  """Read + delete a /tmp/<subdir>/<session>.json nudge state file."""
472
- nudge_file = Path("/tmp") / subdir / f"{session_id}.json"
473
+ nudge_file = arkaos_temp_dir(subdir) / f"{session_id}.json"
473
474
  if not nudge_file.is_file():
474
475
  return ""
475
476
  nudge = ""
@@ -56,7 +56,7 @@ def index_directory(
56
56
 
57
57
  fhash = file_hash(filepath)
58
58
 
59
- if skip_indexed and store.is_file_indexed(fhash):
59
+ if skip_indexed and store.is_file_indexed(str(filepath), fhash):
60
60
  skipped += 1
61
61
  continue
62
62
 
@@ -347,10 +347,17 @@ class VectorStore:
347
347
  for r in rows
348
348
  ]
349
349
 
350
- def is_file_indexed(self, file_hash: str) -> bool:
351
- """Check if a file has already been indexed."""
350
+ def is_file_indexed(self, source: str, file_hash: str) -> bool:
351
+ """Check if a file (identified by source path + content hash) is indexed.
352
+
353
+ The source path is part of the key so identical content in two
354
+ different locations (e.g. two npx cache directories) each gets
355
+ indexed — a hash-only check would skip every subsequent cache
356
+ and leave the new location unsearchable.
357
+ """
352
358
  row = self._db.execute(
353
- "SELECT COUNT(*) as cnt FROM chunks WHERE file_hash = ?", (file_hash,)
359
+ "SELECT COUNT(*) as cnt FROM chunks WHERE source = ? AND file_hash = ?",
360
+ (source, file_hash),
354
361
  ).fetchone()
355
362
  return row["cnt"] > 0
356
363
 
@@ -33,9 +33,10 @@ from typing import Any
33
33
  from core.runtime.llm_cost_telemetry import record_cost
34
34
  from core.runtime.pricing import estimate_cost_usd
35
35
  from core.shared.safe_session_id import safe_session_id
36
+ from core.shared.temp_paths import arkaos_temp_dir
36
37
 
37
38
 
38
- DEFAULT_CURSOR_DIR = Path("/tmp/arkaos-native-cost")
39
+ DEFAULT_CURSOR_DIR = arkaos_temp_dir("arkaos-native-cost")
39
40
 
40
41
  _USAGE_KEYS = (
41
42
  "input_tokens",