arkaos 2.76.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 +1 -1
- package/core/governance/__pycache__/closing_marker_check.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
- package/core/runtime/codex_cli.py +22 -13
- package/core/runtime/llm_provider.py +15 -0
- package/core/sync/__pycache__/update_orchestrator.cpython-313.pyc +0 -0
- package/core/sync/update_orchestrator.py +244 -0
- package/departments/ops/skills/update/SKILL.md +20 -4
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.78.0
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -76,10 +76,16 @@ class CodexCliAdapter(RuntimeAdapter):
|
|
|
76
76
|
raise NotImplementedError("Use Codex CLI's native content search")
|
|
77
77
|
|
|
78
78
|
def headless_supported(self) -> bool:
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
|
|
79
|
+
# Auto-detect: headless is supported iff the `codex` binary is
|
|
80
|
+
# on PATH. When the operator installs Codex CLI later, this
|
|
81
|
+
# lights up without any code change (the headless_complete()
|
|
82
|
+
# method below already gates on shutil.which() too, so a missing
|
|
83
|
+
# binary will raise cleanly).
|
|
84
|
+
#
|
|
85
|
+
# Note: even when the binary is present, headless_complete()
|
|
86
|
+
# still raises until the invocation syntax is verified locally.
|
|
87
|
+
# See TODO(llm-agnostic) below for the verification checklist.
|
|
88
|
+
return shutil.which("codex") is not None
|
|
83
89
|
|
|
84
90
|
def headless_complete(
|
|
85
91
|
self,
|
|
@@ -96,14 +102,15 @@ class CodexCliAdapter(RuntimeAdapter):
|
|
|
96
102
|
)
|
|
97
103
|
# TODO(llm-agnostic): Implement real headless completion.
|
|
98
104
|
#
|
|
99
|
-
# Status as of 2026-
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
105
|
+
# Status as of 2026-05-25 (PR60): Codex CLI still not verified
|
|
106
|
+
# in any ArkaOS dev environment. headless_supported() now
|
|
107
|
+
# auto-detects the binary on PATH so this lights up the moment
|
|
108
|
+
# someone installs it — but the actual subprocess call below
|
|
109
|
+
# still needs syntax verification before we can stop refusing.
|
|
103
110
|
#
|
|
104
111
|
# Verification checklist for whoever picks this up:
|
|
105
112
|
# 1. Install: npm install -g @openai/codex-cli
|
|
106
|
-
# 2. Discover: codex --help
|
|
113
|
+
# 2. Discover: codex --help (confirm non-interactive flag)
|
|
107
114
|
# 3. Pattern: likely `codex exec "<prompt>"` or
|
|
108
115
|
# `codex --prompt "<prompt>" --format json`
|
|
109
116
|
# 4. Wire the subprocess call (mirror the Gemini adapter —
|
|
@@ -113,9 +120,11 @@ class CodexCliAdapter(RuntimeAdapter):
|
|
|
113
120
|
# SubagentProvider cleanly falls back to anthropic-direct or
|
|
114
121
|
# stub when this raises, so the chain keeps working.
|
|
115
122
|
raise NotImplementedError(
|
|
116
|
-
"Codex CLI headless mode requires
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
"
|
|
123
|
+
"Codex CLI headless mode requires verified invocation syntax. "
|
|
124
|
+
"The `codex` binary is on PATH but ArkaOS has not validated "
|
|
125
|
+
"the non-interactive call shape locally. "
|
|
126
|
+
"Verification steps: `codex --help`, then update "
|
|
127
|
+
"core/runtime/codex_cli.py::headless_complete to call the "
|
|
128
|
+
"discovered subprocess shape. "
|
|
120
129
|
"SubagentProvider will cleanly fall back to anthropic-direct or stub."
|
|
121
130
|
)
|
|
@@ -362,6 +362,19 @@ def get_llm_provider(config_path: Path | None = None) -> LLMProvider:
|
|
|
362
362
|
return last if last is not None else StubProvider()
|
|
363
363
|
|
|
364
364
|
|
|
365
|
+
def _current_category() -> str:
|
|
366
|
+
"""Resolve the per-call category from the environment.
|
|
367
|
+
|
|
368
|
+
PR60 v2.77.0 — orchestration layers can set
|
|
369
|
+
``ARKA_CALL_CATEGORY=skill:<slug>`` /
|
|
370
|
+
``subagent:<dept>`` / ``plugin:<id>`` / ``mcp:<server>`` before
|
|
371
|
+
invoking the provider so `/arka costs --by-category` (PR47) can
|
|
372
|
+
attribute spend. Returns "" when unset, which lands in the base
|
|
373
|
+
bucket (backward-compatible).
|
|
374
|
+
"""
|
|
375
|
+
return os.environ.get("ARKA_CALL_CATEGORY", "").strip()
|
|
376
|
+
|
|
377
|
+
|
|
365
378
|
def _log_fallback(preferred: str, selected: str, reason: str = "") -> None:
|
|
366
379
|
# Piggy-back on the cost telemetry file: zero-token, provider-only row.
|
|
367
380
|
# Downstream can group by provider to spot degraded chains.
|
|
@@ -373,6 +386,7 @@ def _log_fallback(preferred: str, selected: str, reason: str = "") -> None:
|
|
|
373
386
|
tokens_out=0,
|
|
374
387
|
cached_tokens=0,
|
|
375
388
|
estimated_cost_usd=None,
|
|
389
|
+
category=_current_category(),
|
|
376
390
|
)
|
|
377
391
|
|
|
378
392
|
|
|
@@ -391,4 +405,5 @@ def _record(session_id: str, provider: str, response: LLMResponse) -> None:
|
|
|
391
405
|
tokens_out=response.tokens_out,
|
|
392
406
|
cached_tokens=response.cached_tokens,
|
|
393
407
|
estimated_cost_usd=cost,
|
|
408
|
+
category=_current_category(),
|
|
394
409
|
)
|
|
Binary file
|
|
@@ -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. **
|
|
24
|
+
1. **One-stop: npm refresh + engine (PR61 v2.78.0 orchestrator).**
|
|
25
25
|
```bash
|
|
26
|
-
cd $ARKAOS_ROOT && python -m core.sync.
|
|
26
|
+
cd $ARKAOS_ROOT && python -m core.sync.update_orchestrator --home ~/.arkaos --skills ~/.claude/skills --output json
|
|
27
27
|
```
|
|
28
|
-
|
|
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