arkaos 4.3.0 → 4.3.2
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/README.md +1 -1
- package/VERSION +1 -1
- package/arka/SKILL.md +4 -4
- package/arka/skills/costs/SKILL.md +1 -1
- package/arka/skills/fusion/SKILL.md +13 -6
- package/config/claude-agents/marta-cqo.md +1 -1
- package/config/claude-agents/paulo-tech-lead.md +1 -1
- package/config/constitution.yaml +5 -5
- package/config/hooks/_lib/arka_python.ps1 +43 -0
- package/config/hooks/_lib/arka_python.sh +63 -0
- package/config/hooks/agent-provision.sh +8 -1
- package/config/hooks/cwd-changed.sh +6 -2
- package/config/hooks/post-tool-use-v2.sh +6 -2
- package/config/hooks/post-tool-use.sh +8 -17
- package/config/hooks/pre-tool-use.ps1 +8 -6
- package/config/hooks/pre-tool-use.sh +8 -17
- package/config/hooks/session-start.sh +40 -15
- package/config/hooks/stop.ps1 +7 -5
- package/config/hooks/stop.sh +8 -17
- package/config/hooks/token-hygiene.sh +5 -1
- package/config/hooks/user-prompt-submit-v2.sh +13 -9
- package/config/hooks/user-prompt-submit.sh +8 -17
- package/core/fusion/__pycache__/cli.cpython-313.pyc +0 -0
- package/core/fusion/__pycache__/panel_builder.cpython-313.pyc +0 -0
- package/core/fusion/cli.py +57 -6
- package/core/fusion/panel_builder.py +58 -0
- package/core/hooks/__pycache__/post_tool_use.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/pre_tool_use.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/pre_tool_use.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/core/hooks/post_tool_use.py +26 -12
- package/core/hooks/pre_tool_use.py +4 -0
- package/core/hooks/user_prompt_submit.py +7 -0
- package/core/memory/__pycache__/rehydrator.cpython-312.pyc +0 -0
- package/core/memory/__pycache__/session_store.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/model_router.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/model_routing_context.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/openrouter_provider.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/runtime_models.cpython-313.pyc +0 -0
- package/core/runtime/model_router.py +17 -0
- package/core/runtime/model_routing_context.py +82 -0
- package/core/runtime/runtime_models.py +51 -0
- package/core/workflow/__pycache__/flow_authorization.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/flow_authorization.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-314.pyc +0 -0
- package/core/workflow/flow_authorization.py +181 -0
- package/core/workflow/flow_enforcer.py +56 -11
- package/core/workflow/state_reader.sh +12 -5
- package/dashboard/app/pages/models.vue +23 -4
- package/departments/ops/skills/update/SKILL.md +2 -2
- package/departments/ops/skills/update/references/sync-engine.md +1 -1
- package/departments/quality/SKILL.md +1 -1
- package/installer/cli.js +21 -0
- package/installer/index.js +14 -0
- package/installer/update.js +13 -0
- package/package.json +5 -2
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/dashboard-api.py +13 -2
- package/scripts/seed_initial_patterns.py +1 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Persistent per-session flow authorization — enforcer resilience.
|
|
2
|
+
|
|
3
|
+
Claude Code exposes NO hook access to the current assistant message
|
|
4
|
+
(confirmed against the hook docs, 2026-07-05: neither PreToolUse nor
|
|
5
|
+
PostToolUse carries the message text, and the transcript at
|
|
6
|
+
``transcript_path`` does not yet include the in-progress turn). So the
|
|
7
|
+
current turn's ``[arka:routing]`` marker is structurally invisible to
|
|
8
|
+
that turn's own tool calls, and the old PostToolUse marker cache read a
|
|
9
|
+
non-existent ``assistant_message`` field — it was never written, leaving
|
|
10
|
+
the enforcer with only a 20-message transcript window that a long
|
|
11
|
+
session or a context compaction empties (652 false-positive blocks
|
|
12
|
+
observed in one session).
|
|
13
|
+
|
|
14
|
+
This module rebuilds resilience on what hooks CAN see: markers observed
|
|
15
|
+
in the transcript (past turns) plus persistent ``/tmp`` state that
|
|
16
|
+
survives compaction and window-rolling.
|
|
17
|
+
|
|
18
|
+
Two tiers:
|
|
19
|
+
|
|
20
|
+
- **CONFIRMED** — written whenever a flow marker is actually observed in
|
|
21
|
+
the transcript. Valid for a session TTL, survives compaction. Grants
|
|
22
|
+
a silent allow. This is the normal steady state: route once, then
|
|
23
|
+
every subsequent turn is authorised.
|
|
24
|
+
- **TURN GRACE** — the current turn's marker cannot be seen, so the very
|
|
25
|
+
first turn of a session (before any marker reaches the transcript) has
|
|
26
|
+
no confirmed auth. Rather than a false-positive block, the turn is
|
|
27
|
+
allowed WITH A WARNING and a per-turn grace flag so the rest of the
|
|
28
|
+
turn proceeds. A never-routing session is warned every turn and, after
|
|
29
|
+
``GRACE_CAP`` consecutive graced turns with no confirmation, escalates
|
|
30
|
+
to a hard block — a normally-routing session confirms by turn 2 and
|
|
31
|
+
never reaches the cap.
|
|
32
|
+
|
|
33
|
+
State dir: ``/tmp/arkaos-flow-auth`` (override via ``ARKA_FLOW_AUTH_DIR``).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
import os
|
|
40
|
+
import time
|
|
41
|
+
from dataclasses import dataclass
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
from core.shared import safe_session_id as _safe_session_id_module
|
|
45
|
+
|
|
46
|
+
_safe_session_id = _safe_session_id_module.safe_session_id
|
|
47
|
+
|
|
48
|
+
# A working session; after this a stale /tmp record expires on its own.
|
|
49
|
+
DEFAULT_TTL_SECONDS = 12 * 60 * 60
|
|
50
|
+
# Consecutive graced turns with no confirmation before a hard block.
|
|
51
|
+
GRACE_CAP = 3
|
|
52
|
+
|
|
53
|
+
VALID_MARKER_TYPES: frozenset[str] = frozenset(
|
|
54
|
+
{"routing", "trivial", "gate", "phase"}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class GraceState:
|
|
60
|
+
count: int
|
|
61
|
+
escalate: bool
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _base_dir() -> Path:
|
|
65
|
+
override = os.environ.get("ARKA_FLOW_AUTH_DIR", "").strip()
|
|
66
|
+
return Path(override) if override else Path("/tmp/arkaos-flow-auth")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _auth_path(session_id: str) -> Path | None:
|
|
70
|
+
safe = _safe_session_id(session_id)
|
|
71
|
+
return (_base_dir() / f"{safe}.json") if safe else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _read(path: Path | None) -> dict:
|
|
75
|
+
if path is None or not path.exists():
|
|
76
|
+
return {}
|
|
77
|
+
try:
|
|
78
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
79
|
+
except (json.JSONDecodeError, OSError):
|
|
80
|
+
return {}
|
|
81
|
+
return data if isinstance(data, dict) else {}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _write(path: Path | None, data: dict) -> None:
|
|
85
|
+
if path is None:
|
|
86
|
+
return
|
|
87
|
+
try:
|
|
88
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
path.write_text(json.dumps(data), encoding="utf-8")
|
|
90
|
+
except OSError:
|
|
91
|
+
pass # authorization state must never block the hook
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def confirm(session_id: str, marker_type: str) -> None:
|
|
95
|
+
"""Persist a confirmed authorization (a marker was observed)."""
|
|
96
|
+
if marker_type not in VALID_MARKER_TYPES:
|
|
97
|
+
return
|
|
98
|
+
path = _auth_path(session_id)
|
|
99
|
+
if path is None:
|
|
100
|
+
return
|
|
101
|
+
_write(path, {
|
|
102
|
+
"marker_type": marker_type,
|
|
103
|
+
"confirmed_ts": time.time(),
|
|
104
|
+
"grace_count": 0, # confirmation clears accumulated grace
|
|
105
|
+
"turn_grace": False,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def confirmed_record(
|
|
110
|
+
session_id: str, ttl_seconds: int = DEFAULT_TTL_SECONDS
|
|
111
|
+
) -> dict | None:
|
|
112
|
+
"""Return the confirmed record if present and within TTL, else None."""
|
|
113
|
+
data = _read(_auth_path(session_id))
|
|
114
|
+
ts = data.get("confirmed_ts")
|
|
115
|
+
if ts is None:
|
|
116
|
+
return None
|
|
117
|
+
if time.time() - float(ts) > ttl_seconds:
|
|
118
|
+
return None
|
|
119
|
+
return data
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def is_confirmed(
|
|
123
|
+
session_id: str, ttl_seconds: int = DEFAULT_TTL_SECONDS
|
|
124
|
+
) -> bool:
|
|
125
|
+
return confirmed_record(session_id, ttl_seconds) is not None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def grant_turn_grace(session_id: str) -> None:
|
|
129
|
+
"""Flag the current turn as graced so its remaining tools pass."""
|
|
130
|
+
path = _auth_path(session_id)
|
|
131
|
+
if path is None:
|
|
132
|
+
return
|
|
133
|
+
data = _read(path)
|
|
134
|
+
data["turn_grace"] = True
|
|
135
|
+
_write(path, data)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def has_turn_grace(session_id: str) -> bool:
|
|
139
|
+
return bool(_read(_auth_path(session_id)).get("turn_grace"))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def reset_turn(session_id: str) -> None:
|
|
143
|
+
"""Clear the per-turn grace flag (called at each new user prompt).
|
|
144
|
+
|
|
145
|
+
Confirmed authorization and the grace counter are preserved — only
|
|
146
|
+
the within-turn flag resets, so the next turn re-evaluates.
|
|
147
|
+
"""
|
|
148
|
+
path = _auth_path(session_id)
|
|
149
|
+
if path is None:
|
|
150
|
+
return
|
|
151
|
+
data = _read(path)
|
|
152
|
+
if data.get("turn_grace"):
|
|
153
|
+
data["turn_grace"] = False
|
|
154
|
+
_write(path, data)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def grace_count(session_id: str) -> int:
|
|
158
|
+
return int(_read(_auth_path(session_id)).get("grace_count", 0) or 0)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def register_grace(session_id: str) -> GraceState:
|
|
162
|
+
"""Increment the consecutive-grace counter; escalate past the cap."""
|
|
163
|
+
path = _auth_path(session_id)
|
|
164
|
+
if path is None:
|
|
165
|
+
return GraceState(count=0, escalate=False)
|
|
166
|
+
data = _read(path)
|
|
167
|
+
count = int(data.get("grace_count", 0) or 0) + 1
|
|
168
|
+
data["grace_count"] = count
|
|
169
|
+
_write(path, data)
|
|
170
|
+
return GraceState(count=count, escalate=count > GRACE_CAP)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def clear(session_id: str) -> None:
|
|
174
|
+
"""Remove all authorization state for a session (tests / session end)."""
|
|
175
|
+
path = _auth_path(session_id)
|
|
176
|
+
if path is None:
|
|
177
|
+
return
|
|
178
|
+
try:
|
|
179
|
+
path.unlink()
|
|
180
|
+
except (FileNotFoundError, OSError):
|
|
181
|
+
pass
|
|
@@ -25,7 +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.workflow import marker_cache
|
|
28
|
+
from core.workflow import flow_authorization, marker_cache
|
|
29
29
|
|
|
30
30
|
try:
|
|
31
31
|
import fcntl # POSIX only
|
|
@@ -222,15 +222,24 @@ class Decision:
|
|
|
222
222
|
marker_found: str | None = None
|
|
223
223
|
phase_observed: str | None = None
|
|
224
224
|
bypass_used: bool = False
|
|
225
|
+
warning: str = ""
|
|
225
226
|
|
|
226
227
|
def to_stderr_message(self) -> str:
|
|
227
228
|
if self.allow:
|
|
228
|
-
return
|
|
229
|
+
return self.warning
|
|
230
|
+
# Recovery that actually works, given Claude Code exposes no
|
|
231
|
+
# current-message text to hooks (the inline `ARKA_BYPASS_FLOW=1`
|
|
232
|
+
# never reached this separate hook process — a documented trap).
|
|
229
233
|
return (
|
|
230
|
-
f"[ARKA:ENFORCEMENT]
|
|
231
|
-
f"
|
|
232
|
-
f"
|
|
233
|
-
f"
|
|
234
|
+
f"[ARKA:ENFORCEMENT] No routing marker for "
|
|
235
|
+
f"{flow_authorization.GRACE_CAP}+ turns. Emit "
|
|
236
|
+
f"`[arka:routing] <dept> -> <lead>` (or `[arka:trivial] "
|
|
237
|
+
f"<reason>`) at the START of your reply — it authorises this "
|
|
238
|
+
f"turn and every turn after. To turn enforcement off set "
|
|
239
|
+
f"hooks.hardEnforcement=false in ~/.arkaos/config.json; to "
|
|
240
|
+
f"bypass programmatically export ARKA_BYPASS_FLOW=1 in Claude "
|
|
241
|
+
f"Code's own environment (settings.json env), not inline on a "
|
|
242
|
+
f"command. Reason: {self.reason}."
|
|
234
243
|
)
|
|
235
244
|
|
|
236
245
|
|
|
@@ -414,18 +423,54 @@ def evaluate(
|
|
|
414
423
|
)
|
|
415
424
|
marker_found, phase_observed = _scan_markers(messages)
|
|
416
425
|
|
|
417
|
-
if marker_found is None:
|
|
426
|
+
if marker_found is not None:
|
|
427
|
+
# Persist a confirmed authorization so this survives compaction and
|
|
428
|
+
# the 20-message window rolling — the fix for the 652 false blocks.
|
|
429
|
+
flow_authorization.confirm(session_id, marker_found)
|
|
418
430
|
return Decision(
|
|
419
|
-
allow=
|
|
420
|
-
reason=f"
|
|
431
|
+
allow=True,
|
|
432
|
+
reason=f"marker-found:{marker_found}",
|
|
433
|
+
marker_found=marker_found,
|
|
421
434
|
phase_observed=phase_observed,
|
|
422
435
|
)
|
|
423
436
|
|
|
437
|
+
# No marker in the transcript window. The current turn's marker is
|
|
438
|
+
# structurally invisible to hooks, so fall back to persistent auth
|
|
439
|
+
# before ever blocking.
|
|
440
|
+
if flow_authorization.is_confirmed(session_id):
|
|
441
|
+
return Decision(
|
|
442
|
+
allow=True, reason="session-authorized", phase_observed=phase_observed
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Same turn, already graced: let the rest of the turn's tools through.
|
|
446
|
+
if flow_authorization.has_turn_grace(session_id):
|
|
447
|
+
return Decision(
|
|
448
|
+
allow=True, reason="turn-grace", phase_observed=phase_observed
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# First effect-tool of a turn with no confirmed auth. A hard deny here
|
|
452
|
+
# is a false positive (the assistant may have routed in this very
|
|
453
|
+
# message — invisible to us). Grace it with a warning; escalate to a
|
|
454
|
+
# real block only after GRACE_CAP consecutive graced turns without any
|
|
455
|
+
# confirmation (a normally-routing session confirms by turn 2).
|
|
456
|
+
grace = flow_authorization.register_grace(session_id)
|
|
457
|
+
if grace.escalate:
|
|
458
|
+
return Decision(
|
|
459
|
+
allow=False,
|
|
460
|
+
reason=f"no-marker-and-grace-exhausted-after-{grace.count}-turns",
|
|
461
|
+
phase_observed=phase_observed,
|
|
462
|
+
)
|
|
463
|
+
flow_authorization.grant_turn_grace(session_id)
|
|
424
464
|
return Decision(
|
|
425
465
|
allow=True,
|
|
426
|
-
reason=f"
|
|
427
|
-
marker_found=marker_found,
|
|
466
|
+
reason=f"first-tool-grace:{grace.count}",
|
|
428
467
|
phase_observed=phase_observed,
|
|
468
|
+
warning=(
|
|
469
|
+
f"[arka:suggest] Effect tool allowed without an observable "
|
|
470
|
+
f"routing marker (grace {grace.count}/{flow_authorization.GRACE_CAP}). "
|
|
471
|
+
f"Emit `[arka:routing] <dept> -> <lead>` at the start of your "
|
|
472
|
+
f"reply to authorise this session."
|
|
473
|
+
),
|
|
429
474
|
)
|
|
430
475
|
|
|
431
476
|
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
# Dependencies: jq (required)
|
|
6
6
|
# ============================================================================
|
|
7
7
|
|
|
8
|
+
# ─── Shared Python resolver (exports ARKA_PY) ──────────────────────────
|
|
9
|
+
# The lib is not a sibling here; try the in-repo path then the installed tree.
|
|
10
|
+
for _l in "$(dirname "${BASH_SOURCE[0]:-$0}")/../../config/hooks/_lib/arka_python.sh" "$HOME/.arkaos/config/hooks/_lib/arka_python.sh"; do
|
|
11
|
+
if [ -f "$_l" ]; then . "$_l"; break; fi
|
|
12
|
+
done
|
|
13
|
+
: "${ARKA_PY:=python3}"
|
|
14
|
+
|
|
8
15
|
STATE_FILE="$HOME/.arkaos/workflow-state.json"
|
|
9
16
|
|
|
10
17
|
if [ ! -f "$STATE_FILE" ]; then
|
|
@@ -73,11 +80,11 @@ case "$CMD" in
|
|
|
73
80
|
echo "none"
|
|
74
81
|
exit 0
|
|
75
82
|
fi
|
|
76
|
-
if command -v
|
|
77
|
-
_F_NAME=$(FORGE_FILE="$_FORGE_FILE"
|
|
78
|
-
_F_STATUS=$(FORGE_FILE="$_FORGE_FILE"
|
|
79
|
-
_F_PHASES=$(FORGE_FILE="$_FORGE_FILE"
|
|
80
|
-
_F_BRANCH=$(FORGE_FILE="$_FORGE_FILE"
|
|
83
|
+
if command -v "$ARKA_PY" >/dev/null 2>&1; then
|
|
84
|
+
_F_NAME=$(FORGE_FILE="$_FORGE_FILE" "$ARKA_PY" -c "import yaml,os; d=yaml.safe_load(open(os.environ['FORGE_FILE'])); print(d.get('name',''))" 2>/dev/null)
|
|
85
|
+
_F_STATUS=$(FORGE_FILE="$_FORGE_FILE" "$ARKA_PY" -c "import yaml,os; d=yaml.safe_load(open(os.environ['FORGE_FILE'])); print(d.get('status',''))" 2>/dev/null)
|
|
86
|
+
_F_PHASES=$(FORGE_FILE="$_FORGE_FILE" "$ARKA_PY" -c "import yaml,os; d=yaml.safe_load(open(os.environ['FORGE_FILE'])); print(len(d.get('plan_phases',[])))" 2>/dev/null)
|
|
87
|
+
_F_BRANCH=$(FORGE_FILE="$_FORGE_FILE" "$ARKA_PY" -c "import yaml,os; d=yaml.safe_load(open(os.environ['FORGE_FILE'])); print(d.get('governance',{}).get('branch_strategy',''))" 2>/dev/null)
|
|
81
88
|
echo "${_FORGE_ID}|${_F_NAME}|${_F_STATUS}|${_F_PHASES}|${_F_BRANCH}"
|
|
82
89
|
else
|
|
83
90
|
echo "${_FORGE_ID}|||0|"
|
|
@@ -8,6 +8,14 @@ interface ResolvedRole {
|
|
|
8
8
|
model: string
|
|
9
9
|
effort: string
|
|
10
10
|
source: string
|
|
11
|
+
description?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface RuntimeModel {
|
|
15
|
+
value: string
|
|
16
|
+
label: string
|
|
17
|
+
tier: string
|
|
18
|
+
note: string
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
interface OllamaModel {
|
|
@@ -24,6 +32,7 @@ interface ModelsOverview {
|
|
|
24
32
|
providers: Record<string, { type: string }>
|
|
25
33
|
aliases: Record<string, Record<string, string>>
|
|
26
34
|
fusion: { judge: Record<string, string>, panel: Array<Record<string, string>> }
|
|
35
|
+
runtime: { id: string, models: RuntimeModel[] }
|
|
27
36
|
ollama: { installed: boolean, running: boolean, host: string, models: OllamaModel[] }
|
|
28
37
|
keys: { openrouter: boolean, anthropic: boolean }
|
|
29
38
|
}
|
|
@@ -119,8 +128,13 @@ const targetOptions = computed(() => {
|
|
|
119
128
|
const o = overview.value
|
|
120
129
|
if (!o) return []
|
|
121
130
|
const options: { label: string, value: string }[] = []
|
|
131
|
+
// Concrete runtime models first (Fable 5, Opus, Sonnet, Haiku) so the
|
|
132
|
+
// operator picks a real model, not just an abstract alias.
|
|
133
|
+
for (const m of o.runtime?.models ?? []) {
|
|
134
|
+
options.push({ label: `runtime / ${m.label} · ${m.note}`, value: `runtime/${m.value}` })
|
|
135
|
+
}
|
|
122
136
|
for (const alias of ['best', 'default', 'fast']) {
|
|
123
|
-
options.push({ label: `runtime / ${alias}`, value: `runtime/${alias}` })
|
|
137
|
+
options.push({ label: `runtime / ${alias} (alias)`, value: `runtime/${alias}` })
|
|
124
138
|
}
|
|
125
139
|
for (const m of o.ollama?.models ?? []) {
|
|
126
140
|
options.push({ label: `ollama / ${m.name}`, value: `ollama/${m.name}` })
|
|
@@ -148,7 +162,7 @@ async function saveEdit(role: string) {
|
|
|
148
162
|
}
|
|
149
163
|
saving.value = true
|
|
150
164
|
try {
|
|
151
|
-
await $fetch(`${apiBase
|
|
165
|
+
await $fetch(`${apiBase}/api/models/role`, {
|
|
152
166
|
method: 'POST',
|
|
153
167
|
body: { role, target: editTarget.value },
|
|
154
168
|
})
|
|
@@ -269,10 +283,15 @@ function formatCost(value: number | null): string {
|
|
|
269
283
|
>
|
|
270
284
|
<UIcon
|
|
271
285
|
:name="QUALITY_ROLES.has(row.role) ? 'i-lucide-gem' : 'i-lucide-cog'"
|
|
272
|
-
class="size-4 shrink-0"
|
|
286
|
+
class="size-4 shrink-0 mt-0.5 self-start"
|
|
273
287
|
:class="QUALITY_ROLES.has(row.role) ? 'text-primary' : 'text-muted'"
|
|
274
288
|
/>
|
|
275
|
-
<
|
|
289
|
+
<div class="w-56 shrink-0">
|
|
290
|
+
<p class="text-sm font-medium">{{ row.role }}</p>
|
|
291
|
+
<p v-if="row.description" class="text-xs text-muted leading-snug">
|
|
292
|
+
{{ row.description }}
|
|
293
|
+
</p>
|
|
294
|
+
</div>
|
|
276
295
|
<template v-if="editingRole === row.role">
|
|
277
296
|
<UInputMenu
|
|
278
297
|
v-model="editTarget"
|
|
@@ -23,7 +23,7 @@ Hybrid sync engine: Python handles deterministic operations (MCPs, settings, des
|
|
|
23
23
|
|
|
24
24
|
1. **One-stop: npm refresh + engine (PR61 v2.78.0 orchestrator).**
|
|
25
25
|
```bash
|
|
26
|
-
cd $ARKAOS_ROOT &&
|
|
26
|
+
cd $ARKAOS_ROOT && ~/.arkaos/bin/arka-py -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
29
|
latest. When stale, it shells out to `npx arkaos@latest update`
|
|
@@ -38,7 +38,7 @@ Hybrid sync engine: Python handles deterministic operations (MCPs, settings, des
|
|
|
38
38
|
silently skips the npm step and falls through to the engine.
|
|
39
39
|
|
|
40
40
|
Fallback (no orchestrator): the underlying engine still runs the
|
|
41
|
-
same way via
|
|
41
|
+
same way via `~/.arkaos/bin/arka-py -m core.sync.engine ...` for callers that
|
|
42
42
|
don't need the version-drift gate.
|
|
43
43
|
|
|
44
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.
|
|
@@ -7,7 +7,7 @@ Referenced from SKILL.md. Read only when needed.
|
|
|
7
7
|
Run the sync engine from the ArkaOS repo root:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
cd $ARKAOS_ROOT &&
|
|
10
|
+
cd $ARKAOS_ROOT && ~/.arkaos/bin/arka-py -m core.sync.engine --home ~/.arkaos --skills ~/.claude/skills --output json
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
The engine handles deterministic operations and returns a JSON report.
|
|
@@ -24,7 +24,7 @@ Any Department Workflow:
|
|
|
24
24
|
...
|
|
25
25
|
Phase N-1: QUALITY GATE
|
|
26
26
|
1. Run the evidence engine over the project/diff:
|
|
27
|
-
|
|
27
|
+
~/.arkaos/bin/arka-py -m core.governance.evidence_checks <project_dir> \
|
|
28
28
|
[--changed-files f1,f2] [--test-command '...'] --json
|
|
29
29
|
2. Marta dispatches Eduardo + Francisca to INTERPRET the report:
|
|
30
30
|
- Eduardo: spellcheck section + prose review of changed copy
|
package/installer/cli.js
CHANGED
|
@@ -28,6 +28,9 @@ const { values, positionals } = parseArgs({
|
|
|
28
28
|
// Model Fabric PR-A — `npx arkaos models [--json] [set ... --effort max]`
|
|
29
29
|
json: { type: "boolean" },
|
|
30
30
|
effort: { type: "string" },
|
|
31
|
+
// Fusion — `npx arkaos fusion [--save|--show] "question"`
|
|
32
|
+
save: { type: "boolean" },
|
|
33
|
+
show: { type: "boolean" },
|
|
31
34
|
},
|
|
32
35
|
allowPositionals: true,
|
|
33
36
|
strict: false,
|
|
@@ -177,6 +180,24 @@ async function main() {
|
|
|
177
180
|
break;
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
case "fusion": {
|
|
184
|
+
const { execSync } = await import("node:child_process");
|
|
185
|
+
const repoRootFusion = join(__dirname, "..");
|
|
186
|
+
const pyFusion = getArkaosPython();
|
|
187
|
+
if (!pyFusion) { console.error("No Python found. Run: npx arkaos install"); process.exit(1); }
|
|
188
|
+
const fusionArgs = positionals.slice(1).map((a) => `"${a}"`).join(" ");
|
|
189
|
+
const saveFlag = values.save ? " --save" : "";
|
|
190
|
+
const showFlag = values.show ? " --show" : "";
|
|
191
|
+
try {
|
|
192
|
+
execSync(`"${pyFusion}" -m core.fusion.cli${saveFlag}${showFlag} ${fusionArgs}`, {
|
|
193
|
+
stdio: "inherit",
|
|
194
|
+
cwd: repoRootFusion,
|
|
195
|
+
env: { ...process.env, ARKAOS_ROOT: repoRootFusion, PYTHONPATH: repoRootFusion },
|
|
196
|
+
});
|
|
197
|
+
} catch { process.exit(1); }
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
|
|
180
201
|
case "index": {
|
|
181
202
|
const { execSync } = await import("node:child_process");
|
|
182
203
|
const indexArgs = positionals.slice(1).join(" ");
|
package/installer/index.js
CHANGED
|
@@ -165,6 +165,12 @@ export async function install({ runtime, path, force, skipSystem, withOllama })
|
|
|
165
165
|
copyFileSync(cmdSrc, join(binDir, "arka-claude.cmd"));
|
|
166
166
|
installed = true;
|
|
167
167
|
}
|
|
168
|
+
// arka-py — the ArkaOS Python entrypoint SKILL.md commands invoke.
|
|
169
|
+
const pyPsSrc = join(ARKAOS_ROOT, "bin", "arka-py.ps1");
|
|
170
|
+
const pyCmdSrc = join(ARKAOS_ROOT, "bin", "arka-py.cmd");
|
|
171
|
+
if (existsSync(pyPsSrc)) copyFileSync(pyPsSrc, join(binDir, "arka-py.ps1"));
|
|
172
|
+
if (existsSync(pyCmdSrc)) copyFileSync(pyCmdSrc, join(binDir, "arka-py.cmd"));
|
|
173
|
+
if (existsSync(pyPsSrc) || existsSync(pyCmdSrc)) ok("arka-py interpreter shim installed (.cmd + .ps1)");
|
|
168
174
|
if (installed) {
|
|
169
175
|
ok("arka-claude wrapper installed (.cmd + .ps1)");
|
|
170
176
|
console.log(` Add to PATH: setx PATH "%PATH%;%USERPROFILE%\\.arkaos\\bin"`);
|
|
@@ -179,6 +185,14 @@ export async function install({ runtime, path, force, skipSystem, withOllama })
|
|
|
179
185
|
console.log(` Add to PATH: export PATH="$HOME/.arkaos/bin:$PATH"`);
|
|
180
186
|
console.log(` Optional alias: alias claude="arka-claude"`);
|
|
181
187
|
}
|
|
188
|
+
// arka-py — the ArkaOS Python entrypoint SKILL.md commands invoke so the
|
|
189
|
+
// agent never runs a bare `python` that lacks ArkaOS deps (pyyaml, ...).
|
|
190
|
+
const arkaPySrc = join(ARKAOS_ROOT, "bin", "arka-py");
|
|
191
|
+
if (existsSync(arkaPySrc)) {
|
|
192
|
+
copyFileSync(arkaPySrc, join(binDir, "arka-py"));
|
|
193
|
+
try { chmodSync(join(binDir, "arka-py"), 0o755); } catch {}
|
|
194
|
+
ok("arka-py interpreter shim installed");
|
|
195
|
+
}
|
|
182
196
|
}
|
|
183
197
|
const claudeMdSrc = join(ARKAOS_ROOT, "config", "user-claude.md");
|
|
184
198
|
const userClaudeMd = join(homedir(), ".claude", "CLAUDE.md");
|
package/installer/update.js
CHANGED
|
@@ -248,6 +248,13 @@ export async function update() {
|
|
|
248
248
|
if (existsSync(psSrc) || existsSync(cmdSrc)) {
|
|
249
249
|
console.log(" ✓ arka-claude wrapper updated (.cmd + .ps1)");
|
|
250
250
|
}
|
|
251
|
+
const pyPsSrc = join(ARKAOS_ROOT, "bin", "arka-py.ps1");
|
|
252
|
+
const pyCmdSrc = join(ARKAOS_ROOT, "bin", "arka-py.cmd");
|
|
253
|
+
if (existsSync(pyPsSrc)) copyFileSync(pyPsSrc, join(binDir, "arka-py.ps1"));
|
|
254
|
+
if (existsSync(pyCmdSrc)) copyFileSync(pyCmdSrc, join(binDir, "arka-py.cmd"));
|
|
255
|
+
if (existsSync(pyPsSrc) || existsSync(pyCmdSrc)) {
|
|
256
|
+
console.log(" ✓ arka-py interpreter shim updated (.cmd + .ps1)");
|
|
257
|
+
}
|
|
251
258
|
} else {
|
|
252
259
|
const wrapperSrc = join(ARKAOS_ROOT, "bin", "arka-claude");
|
|
253
260
|
if (existsSync(wrapperSrc)) {
|
|
@@ -255,6 +262,12 @@ export async function update() {
|
|
|
255
262
|
try { chmodSync(join(binDir, "arka-claude"), 0o755); } catch {}
|
|
256
263
|
console.log(" ✓ arka-claude wrapper updated");
|
|
257
264
|
}
|
|
265
|
+
const arkaPySrc = join(ARKAOS_ROOT, "bin", "arka-py");
|
|
266
|
+
if (existsSync(arkaPySrc)) {
|
|
267
|
+
copyFileSync(arkaPySrc, join(binDir, "arka-py"));
|
|
268
|
+
try { chmodSync(join(binDir, "arka-py"), 0o755); } catch {}
|
|
269
|
+
console.log(" ✓ arka-py interpreter shim updated");
|
|
270
|
+
}
|
|
258
271
|
}
|
|
259
272
|
const userClaudeMd = join(homedir(), ".claude", "CLAUDE.md");
|
|
260
273
|
const claudeMdSrc = join(ARKAOS_ROOT, "config", "user-claude.md");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arkaos",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.2",
|
|
4
4
|
"description": "The Operating System for AI Agent Teams",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -58,5 +58,8 @@
|
|
|
58
58
|
"CONSTITUTION.md",
|
|
59
59
|
"README.md",
|
|
60
60
|
"LICENSE"
|
|
61
|
-
]
|
|
61
|
+
],
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"eslint": "^8.57.1"
|
|
64
|
+
}
|
|
62
65
|
}
|
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -867,8 +867,14 @@ def commands(dept: Optional[str] = Query(None), q: Optional[str] = Query(None)):
|
|
|
867
867
|
@app.get("/api/models")
|
|
868
868
|
def models_overview():
|
|
869
869
|
"""Model Fabric: roles, providers, Ollama discovery, key presence."""
|
|
870
|
-
from core.runtime.model_router import
|
|
870
|
+
from core.runtime.model_router import (
|
|
871
|
+
USER_CONFIG_PATH,
|
|
872
|
+
load_config,
|
|
873
|
+
resolve_all,
|
|
874
|
+
role_description,
|
|
875
|
+
)
|
|
871
876
|
from core.runtime.ollama_discovery import discover
|
|
877
|
+
from core.runtime.runtime_models import detect_runtime_models
|
|
872
878
|
|
|
873
879
|
config, source = load_config()
|
|
874
880
|
keys_path = Path.home() / ".arkaos" / "keys.json"
|
|
@@ -876,16 +882,21 @@ def models_overview():
|
|
|
876
882
|
keys = json.loads(keys_path.read_text(encoding="utf-8"))
|
|
877
883
|
except (OSError, json.JSONDecodeError):
|
|
878
884
|
keys = {}
|
|
885
|
+
runtime_id, runtime_models = detect_runtime_models()
|
|
879
886
|
return {
|
|
880
887
|
"source": source,
|
|
881
888
|
"config_path": str(USER_CONFIG_PATH),
|
|
882
|
-
"roles": [
|
|
889
|
+
"roles": [
|
|
890
|
+
{**item.model_dump(), "description": role_description(item.role)}
|
|
891
|
+
for item in resolve_all()
|
|
892
|
+
],
|
|
883
893
|
"providers": {
|
|
884
894
|
name: {"type": spec.get("type", "")}
|
|
885
895
|
for name, spec in config.providers.items()
|
|
886
896
|
},
|
|
887
897
|
"aliases": config.aliases,
|
|
888
898
|
"fusion": config.fusion.model_dump(),
|
|
899
|
+
"runtime": {"id": runtime_id, "models": runtime_models},
|
|
889
900
|
"ollama": discover().to_dict(),
|
|
890
901
|
"keys": {
|
|
891
902
|
"openrouter": bool(
|
|
@@ -175,7 +175,7 @@ def main() -> int:
|
|
|
175
175
|
for card in _INITIAL_PATTERNS:
|
|
176
176
|
record_pattern(card)
|
|
177
177
|
print(f"Seeded {len(_INITIAL_PATTERNS)} pattern cards.")
|
|
178
|
-
print("Inspect:
|
|
178
|
+
print("Inspect: ~/.arkaos/bin/arka-py -m core.knowledge.pattern_cards_cli list")
|
|
179
179
|
return 0
|
|
180
180
|
|
|
181
181
|
|