arkaos 2.77.0 → 2.78.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 CHANGED
@@ -1 +1 @@
1
- 2.77.0
1
+ 2.78.0
@@ -0,0 +1,244 @@
1
+ """One-stop /arka update orchestrator (PR61 v2.78.0).
2
+
3
+ The published 2-step process (`npx arkaos@latest update` then
4
+ `/arka update`) is fragile in practice: operators run step 2 inside
5
+ Claude Code without remembering step 1, so the sync engine silently
6
+ runs from whichever npx cache `~/.arkaos/.repo-path` last pointed at.
7
+ When that cache is months old the sync becomes a no-op against
8
+ current versions.
9
+
10
+ This module makes `/arka update` self-sufficient:
11
+
12
+ 1. Read the running ArkaOS version from `<repo>/VERSION`.
13
+ 2. Probe the npm registry for the published latest (5s timeout,
14
+ 1-hour cache on disk to keep repeat runs cheap).
15
+ 3. If the running version is older than the latest, shell out to
16
+ `npx arkaos@latest update` and wait for it to finish before
17
+ touching the sync engine. The npx step rewrites
18
+ ``~/.arkaos/.repo-path`` to the freshly-extracted package so the
19
+ sync engine below reads the right code.
20
+ 4. Re-read VERSION (now updated) and dispatch to ``run_sync``.
21
+
22
+ The orchestrator NEVER raises on transient failures — npm offline,
23
+ slow registry, missing `npx` — it logs and falls through to the sync
24
+ engine using whatever code is currently installed. Worst case the
25
+ operator sees the same behaviour as before PR61.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ import os
32
+ import subprocess
33
+ import sys
34
+ import time
35
+ from pathlib import Path
36
+ from typing import Optional
37
+
38
+ from core.sync.engine import (
39
+ _read_current_version,
40
+ _read_repo_path,
41
+ run_sync,
42
+ )
43
+ from core.sync.schema import SyncReport
44
+
45
+
46
+ # Cache the npm-view result for an hour so repeated /arka update calls
47
+ # inside the same session don't re-hit the registry.
48
+ _NPM_CACHE_TTL_SECONDS = 3600
49
+ _NPM_TIMEOUT_SECONDS = 5
50
+ _NPM_PROBE_CMD = ("npm", "view", "arkaos", "version")
51
+ _NPX_UPDATE_CMD = ("npx", "-y", "arkaos@latest", "update")
52
+ _NPX_TIMEOUT_SECONDS = 600 # 10 minutes — large installs can be slow
53
+
54
+
55
+ def orchestrate(
56
+ arkaos_home: Path,
57
+ skills_dir: Path,
58
+ home_path: str,
59
+ *,
60
+ npm_probe=None,
61
+ npx_run=None,
62
+ cache_path: Path | None = None,
63
+ ) -> tuple[Optional[str], Optional[str], SyncReport]:
64
+ """Run npm-side update when stale, then the sync engine.
65
+
66
+ Returns ``(installed_version_before, latest_version_seen, report)``.
67
+ The first two are None when probing failed; the third is always a
68
+ SyncReport (the engine itself never raises on individual project
69
+ failures).
70
+ """
71
+ probe = npm_probe or _probe_npm_latest
72
+ runner = npx_run or _run_npx_update
73
+ cache = cache_path or (arkaos_home / "npm-latest.cache.json")
74
+
75
+ installed = _safe_read_version(arkaos_home)
76
+ latest = probe(cache)
77
+
78
+ if installed and latest and _is_older(installed, latest):
79
+ runner(arkaos_home)
80
+
81
+ report = run_sync(
82
+ arkaos_home=arkaos_home,
83
+ skills_dir=skills_dir,
84
+ home_path=home_path,
85
+ )
86
+ return installed, latest, report
87
+
88
+
89
+ def _safe_read_version(arkaos_home: Path) -> Optional[str]:
90
+ try:
91
+ v = _read_current_version(arkaos_home)
92
+ return v if v and v != "unknown" else None
93
+ except Exception: # noqa: BLE001 — never break the orchestrator
94
+ return None
95
+
96
+
97
+ def _probe_npm_latest(cache_path: Path) -> Optional[str]:
98
+ """Return the latest published arkaos version, or None on failure.
99
+
100
+ Reads from disk cache when fresh; otherwise shells out to
101
+ ``npm view`` with a short timeout. Always swallows errors —
102
+ callers fall through to a no-op when probing fails.
103
+ """
104
+ cached = _read_cache(cache_path)
105
+ if cached is not None:
106
+ return cached
107
+ try:
108
+ result = subprocess.run(
109
+ list(_NPM_PROBE_CMD),
110
+ capture_output=True,
111
+ text=True,
112
+ timeout=_NPM_TIMEOUT_SECONDS,
113
+ check=False,
114
+ )
115
+ except (OSError, subprocess.TimeoutExpired):
116
+ return None
117
+ if result.returncode != 0:
118
+ return None
119
+ version = (result.stdout or "").strip()
120
+ if not _looks_like_semver(version):
121
+ return None
122
+ _write_cache(cache_path, version)
123
+ return version
124
+
125
+
126
+ def _read_cache(cache_path: Path) -> Optional[str]:
127
+ if not cache_path.exists():
128
+ return None
129
+ try:
130
+ data = json.loads(cache_path.read_text(encoding="utf-8"))
131
+ except (json.JSONDecodeError, OSError):
132
+ return None
133
+ if not isinstance(data, dict):
134
+ return None
135
+ ts = data.get("ts")
136
+ version = data.get("version")
137
+ if not isinstance(ts, (int, float)) or not isinstance(version, str):
138
+ return None
139
+ if time.time() - ts > _NPM_CACHE_TTL_SECONDS:
140
+ return None
141
+ return version if _looks_like_semver(version) else None
142
+
143
+
144
+ def _write_cache(cache_path: Path, version: str) -> None:
145
+ try:
146
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
147
+ cache_path.write_text(
148
+ json.dumps({"version": version, "ts": time.time()}),
149
+ encoding="utf-8",
150
+ )
151
+ except OSError:
152
+ pass
153
+
154
+
155
+ def _looks_like_semver(value: str) -> bool:
156
+ if not value or len(value) > 32:
157
+ return False
158
+ # Strip any `-prerelease.suffix` so "2.77.0-beta.1" reduces to "2.77.0"
159
+ # before structural validation. npm canonical semver allows almost any
160
+ # ASCII after the dash; we don't try to validate that — we only need
161
+ # to confirm the leading major.minor.patch shape is intact.
162
+ base = value.split("-", 1)[0]
163
+ parts = base.split(".")
164
+ if len(parts) != 3:
165
+ return False
166
+ return all(part.isdigit() for part in parts)
167
+
168
+
169
+ def _is_older(installed: str, latest: str) -> bool:
170
+ return _parse_semver(installed) < _parse_semver(latest)
171
+
172
+
173
+ def _parse_semver(value: str) -> tuple[int, int, int]:
174
+ parts = value.split(".")
175
+ try:
176
+ major = int(parts[0])
177
+ minor = int(parts[1])
178
+ patch_str = parts[2].split("-", 1)[0]
179
+ patch = int(patch_str)
180
+ return major, minor, patch
181
+ except (ValueError, IndexError):
182
+ return (0, 0, 0)
183
+
184
+
185
+ def _run_npx_update(arkaos_home: Path) -> None:
186
+ """Best-effort shell-out to `npx arkaos@latest update`.
187
+
188
+ Inherits the parent's stdout/stderr so the operator sees the same
189
+ installer banner they would in a manual run. Swallows OSError /
190
+ TimeoutExpired so the surrounding sync still runs.
191
+ """
192
+ del arkaos_home # passed for symmetry with _probe_npm_latest signature
193
+ try:
194
+ env = os.environ.copy()
195
+ # Some operators set CI=1 to suppress installer prompts; preserve it.
196
+ subprocess.run(
197
+ list(_NPX_UPDATE_CMD),
198
+ check=False,
199
+ timeout=_NPX_TIMEOUT_SECONDS,
200
+ env=env,
201
+ )
202
+ except (OSError, subprocess.TimeoutExpired) as exc:
203
+ sys.stderr.write(
204
+ f"[arkaos] npx arkaos@latest update failed: {exc}\n"
205
+ "[arkaos] continuing with the sync engine using the "
206
+ "currently-installed core.\n"
207
+ )
208
+
209
+
210
+ def main(argv: list[str]) -> int:
211
+ """CLI entry: python -m core.sync.update_orchestrator --home X --skills Y."""
212
+ import argparse
213
+ parser = argparse.ArgumentParser(description="ArkaOS one-stop /arka update")
214
+ parser.add_argument("--home", required=True)
215
+ parser.add_argument("--skills", required=True)
216
+ parser.add_argument("--output", choices=["text", "json"], default="text")
217
+ args = parser.parse_args(argv[1:])
218
+
219
+ installed, latest, report = orchestrate(
220
+ arkaos_home=Path(args.home),
221
+ skills_dir=Path(args.skills),
222
+ home_path=str(Path.home()),
223
+ )
224
+
225
+ if args.output == "json":
226
+ payload = {
227
+ "installed_version_before": installed,
228
+ "latest_version_seen": latest,
229
+ "report": report.model_dump(),
230
+ }
231
+ print(json.dumps(payload, indent=2))
232
+ else:
233
+ from core.sync.reporter import format_report
234
+ print(f"Installed: {installed or 'unknown'}")
235
+ print(f"Latest published: {latest or 'unknown'}")
236
+ if installed and latest and _is_older(installed, latest):
237
+ print(f"Updated from {installed} → {latest} via npx arkaos@latest")
238
+ print()
239
+ print(format_report(report))
240
+ return 0
241
+
242
+
243
+ if __name__ == "__main__":
244
+ sys.exit(main(sys.argv))
@@ -21,15 +21,31 @@ Hybrid sync engine: Python handles deterministic operations (MCPs, settings, des
21
21
 
22
22
  ## Orchestration (Summary)
23
23
 
24
- 1. **Phases 1–3 + 5 (deterministic, Python):** Run the engine:
24
+ 1. **One-stop: npm refresh + engine (PR61 v2.78.0 orchestrator).**
25
25
  ```bash
26
- cd $ARKAOS_ROOT && python -m core.sync.engine --home ~/.arkaos --skills ~/.claude/skills --output json
26
+ cd $ARKAOS_ROOT && python -m core.sync.update_orchestrator --home ~/.arkaos --skills ~/.claude/skills --output json
27
27
  ```
28
- Handles manifest, discovery, MCP sync, settings sync, descriptors, and writes `~/.arkaos/sync-state.json`.
28
+ The orchestrator detects whether the running ArkaOS is behind npm
29
+ latest. When stale, it shells out to `npx arkaos@latest update`
30
+ first so the sync engine below reads fresh code; when current, it
31
+ skips straight to the engine. Either way it runs the
32
+ deterministic engine (manifest, discovery, MCP sync, settings
33
+ sync, descriptors, content, agents) and writes
34
+ `~/.arkaos/sync-state.json`.
35
+
36
+ Probe is cached for 1 hour in `~/.arkaos/npm-latest.cache.json`
37
+ to keep repeat runs cheap. Offline / `npx` missing → orchestrator
38
+ silently skips the npm step and falls through to the engine.
39
+
40
+ Fallback (no orchestrator): the underlying engine still runs the
41
+ same way via `python -m core.sync.engine ...` for callers that
42
+ don't need the version-drift gate.
29
43
 
30
44
  2. **Phase 4 (intelligent, AI subagent):** After the engine completes, dispatch ONE subagent with the engine's JSON report + the feature registry (`core/sync/features/*.yaml`). The subagent injects/removes feature sections in each `~/.claude/skills/arka-{ecosystem}/SKILL.md` while preserving all custom content.
31
45
 
32
- 3. **Report:** Display the formatted summary returned by the engine.
46
+ 3. **Report:** Display the formatted summary returned by the engine,
47
+ plus `installed_version_before` / `latest_version_seen` from the
48
+ orchestrator so the operator sees what got refreshed.
33
49
 
34
50
  ## Error Handling (Summary)
35
51
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "2.77.0",
3
+ "version": "2.78.0",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "2.77.0"
3
+ version = "2.78.0"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}