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
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
set +e # never fail
|
|
15
15
|
|
|
16
|
+
# ─── Shared Python resolver (exports ARKA_PY) ──────────────────────────
|
|
17
|
+
_ARKA_LIB="$(dirname "${BASH_SOURCE[0]:-$0}")/_lib/arka_python.sh"
|
|
18
|
+
if [ -f "$_ARKA_LIB" ]; then . "$_ARKA_LIB"; else ARKA_PY="python3"; fi
|
|
19
|
+
|
|
16
20
|
_prompt="${ARKA_PROMPT:-}"
|
|
17
21
|
_transcript="${ARKA_TRANSCRIPT_PATH:-}"
|
|
18
22
|
_suggestions=""
|
|
@@ -64,7 +68,7 @@ if [ -n "$_transcript" ] && [ -f "$_transcript" ] && [ -n "$_prompt" ]; then
|
|
|
64
68
|
_cur_kw=$(_kw "$_prompt" | head -20)
|
|
65
69
|
# pull last 3 user messages from transcript jsonl
|
|
66
70
|
_prior=$(tail -n 200 "$_transcript" 2>/dev/null \
|
|
67
|
-
|
|
|
71
|
+
| "$ARKA_PY" -c "
|
|
68
72
|
import sys, json
|
|
69
73
|
msgs=[]
|
|
70
74
|
for line in sys.stdin:
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
input=$(cat)
|
|
9
9
|
|
|
10
|
+
# ─── Shared Python resolver (exports ARKA_PY) ──────────────────────────
|
|
11
|
+
_ARKA_LIB="$(dirname "${BASH_SOURCE[0]:-$0}")/_lib/arka_python.sh"
|
|
12
|
+
if [ -f "$_ARKA_LIB" ]; then . "$_ARKA_LIB"; else ARKA_PY="python3"; fi
|
|
13
|
+
|
|
10
14
|
# ─── V1 Migration Detection ─────────────────────────────────────────────
|
|
11
15
|
V1_PATHS=("$HOME/.claude/skills/arka-os" "$HOME/.claude/skills/arkaos")
|
|
12
16
|
MIGRATION_MARKER="$HOME/.arkaos/migrated-from-v1"
|
|
@@ -27,12 +31,12 @@ if [ -f "$ARKAOS_VERSION_FILE" ]; then
|
|
|
27
31
|
if [ -f "$_REPO_PATH/VERSION" ]; then
|
|
28
32
|
_CURRENT_VERSION=$(cat "$_REPO_PATH/VERSION")
|
|
29
33
|
elif [ -f "$_REPO_PATH/package.json" ]; then
|
|
30
|
-
_CURRENT_VERSION=$(
|
|
34
|
+
_CURRENT_VERSION=$("$ARKA_PY" -c "import json; print(json.load(open('$_REPO_PATH/package.json'))['version'])" 2>/dev/null || echo "")
|
|
31
35
|
fi
|
|
32
36
|
|
|
33
37
|
if [ -n "${_CURRENT_VERSION:-}" ]; then
|
|
34
38
|
if [ -f "$SYNC_STATE" ]; then
|
|
35
|
-
_SYNCED_VERSION=$(
|
|
39
|
+
_SYNCED_VERSION=$("$ARKA_PY" -c "import json; print(json.load(open('$SYNC_STATE'))['version'])" 2>/dev/null || echo "none")
|
|
36
40
|
else
|
|
37
41
|
_SYNCED_VERSION="none"
|
|
38
42
|
fi
|
|
@@ -53,9 +57,9 @@ if [ ! -f "$_SESSION_MARKER" ] && [ ! -f "/tmp/arkaos-greeted-today" ]; then
|
|
|
53
57
|
_GREETING_COMPANY=""
|
|
54
58
|
_GREETING_VERSION=""
|
|
55
59
|
|
|
56
|
-
if [ -f "$HOME/.arkaos/profile.json" ] && command -v
|
|
57
|
-
_GREETING_NAME=$(
|
|
58
|
-
_GREETING_COMPANY=$(
|
|
60
|
+
if [ -f "$HOME/.arkaos/profile.json" ] && command -v "$ARKA_PY" >/dev/null 2>&1; then
|
|
61
|
+
_GREETING_NAME=$("$ARKA_PY" -c "import json; p=json.load(open('$HOME/.arkaos/profile.json')); print(p.get('name', p.get('role', 'founder')))" 2>/dev/null)
|
|
62
|
+
_GREETING_COMPANY=$("$ARKA_PY" -c "import json; print(json.load(open('$HOME/.arkaos/profile.json')).get('company', ''))" 2>/dev/null)
|
|
59
63
|
fi
|
|
60
64
|
|
|
61
65
|
if [ -f "$HOME/.arkaos/.repo-path" ]; then
|
|
@@ -111,12 +115,12 @@ fi
|
|
|
111
115
|
python_result=""
|
|
112
116
|
BRIDGE_SCRIPT="${ARKAOS_ROOT}/scripts/synapse-bridge.py"
|
|
113
117
|
|
|
114
|
-
if command -v
|
|
115
|
-
bridge_output=$(echo "{\"user_input\":$(echo "$user_input" |
|
|
116
|
-
| ARKAOS_ROOT="$ARKAOS_ROOT"
|
|
118
|
+
if command -v "$ARKA_PY" >/dev/null 2>&1 && [ -f "$BRIDGE_SCRIPT" ]; then
|
|
119
|
+
bridge_output=$(echo "{\"user_input\":$(echo "$user_input" | "$ARKA_PY" -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null || echo '""')}" \
|
|
120
|
+
| ARKAOS_ROOT="$ARKAOS_ROOT" "$ARKA_PY" "$BRIDGE_SCRIPT" --root "$ARKAOS_ROOT" 2>/dev/null)
|
|
117
121
|
|
|
118
122
|
if [ -n "$bridge_output" ]; then
|
|
119
|
-
python_result=$(echo "$bridge_output" |
|
|
123
|
+
python_result=$(echo "$bridge_output" | "$ARKA_PY" -c "import sys,json; print(json.loads(sys.stdin.read()).get('context_string',''))" 2>/dev/null)
|
|
120
124
|
fi
|
|
121
125
|
fi
|
|
122
126
|
|
|
@@ -34,9 +34,13 @@ if [ -z "${ARKAOS_ROOT:-}" ]; then
|
|
|
34
34
|
fi
|
|
35
35
|
export ARKAOS_ROOT
|
|
36
36
|
|
|
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
|
+
|
|
37
41
|
_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"}'
|
|
38
42
|
|
|
39
|
-
if ! command -v
|
|
43
|
+
if ! command -v "$ARKA_PY" >/dev/null 2>&1; then
|
|
40
44
|
echo "$_ARKA_L0_FALLBACK"
|
|
41
45
|
exit 0
|
|
42
46
|
fi
|
|
@@ -54,20 +58,7 @@ if [ ! -f "$ARKAOS_ROOT/core/hooks/user_prompt_submit.py" ]; then
|
|
|
54
58
|
fi
|
|
55
59
|
fi
|
|
56
60
|
|
|
57
|
-
# Interpreter resolution
|
|
58
|
-
#
|
|
59
|
-
# python3 without yaml and silently degrade every gate.
|
|
60
|
-
_PY=""
|
|
61
|
-
for _cand in "$HOME/.arkaos/venv/bin/python3" "$HOME/.arkaos/.venv/bin/python3"; do
|
|
62
|
-
if [ -x "$_cand" ]; then _PY="$_cand"; break; fi
|
|
63
|
-
done
|
|
64
|
-
if [ -z "$_PY" ]; then
|
|
65
|
-
_PY="python3"
|
|
66
|
-
if ! "$_PY" -c "import yaml" 2>/dev/null; then
|
|
67
|
-
for _cand in /opt/homebrew/bin/python3 /usr/local/bin/python3 /usr/bin/python3; do
|
|
68
|
-
if [ -x "$_cand" ] && "$_cand" -c "import yaml" 2>/dev/null; then _PY="$_cand"; break; fi
|
|
69
|
-
done
|
|
70
|
-
fi
|
|
71
|
-
fi
|
|
61
|
+
# Interpreter resolution handled by the shared resolver (ARKA_PY): prefers
|
|
62
|
+
# the ArkaOS venv (has pyyaml/pydantic), falls back to a yaml-capable python3.
|
|
72
63
|
PYTHONPATH="$ARKAOS_ROOT${PYTHONPATH:+:$PYTHONPATH}" \
|
|
73
|
-
exec "$
|
|
64
|
+
exec "$ARKA_PY" -m core.hooks.user_prompt_submit
|
|
Binary file
|
|
Binary file
|
package/core/fusion/cli.py
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
"""Fusion CLI —
|
|
1
|
+
"""Fusion CLI — run the panel → judge → synthesis pipeline.
|
|
2
2
|
|
|
3
3
|
Usage::
|
|
4
4
|
|
|
5
5
|
python -m core.fusion.cli "your question"
|
|
6
|
+
python -m core.fusion.cli --save "your question" # persist the panel
|
|
7
|
+
python -m core.fusion.cli --show # show the panel only
|
|
8
|
+
|
|
9
|
+
When ``models.yaml`` has no ``fusion.panel``, a default is built from the
|
|
10
|
+
machine's models (runtime + local Ollama) so fusion works out of the box.
|
|
11
|
+
``--save`` writes that panel into ``~/.arkaos/models.yaml`` so every
|
|
12
|
+
fusion consumer (and the dashboard) sees it.
|
|
6
13
|
"""
|
|
7
14
|
|
|
8
15
|
from __future__ import annotations
|
|
@@ -10,16 +17,60 @@ from __future__ import annotations
|
|
|
10
17
|
import sys
|
|
11
18
|
|
|
12
19
|
from core.fusion.engine import FusionUnavailable, fuse
|
|
20
|
+
from core.fusion.panel_builder import default_panel, describe_panel
|
|
21
|
+
from core.runtime.model_router import load_config
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _save_panel(panel, judge) -> str:
|
|
25
|
+
import yaml
|
|
26
|
+
from core.runtime.model_router import USER_CONFIG_PATH, ensure_user_config
|
|
27
|
+
path = ensure_user_config()
|
|
28
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
29
|
+
data.setdefault("fusion", {})
|
|
30
|
+
data["fusion"]["judge"] = judge.model_dump()
|
|
31
|
+
data["fusion"]["panel"] = [c.model_dump() for c in panel]
|
|
32
|
+
path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8")
|
|
33
|
+
return str(USER_CONFIG_PATH)
|
|
13
34
|
|
|
14
35
|
|
|
15
36
|
def main(argv: list[str] | None = None) -> int:
|
|
16
|
-
args = argv if argv is not None else sys.argv[1:]
|
|
17
|
-
|
|
18
|
-
|
|
37
|
+
args = list(argv if argv is not None else sys.argv[1:])
|
|
38
|
+
save = "--save" in args
|
|
39
|
+
show = "--show" in args
|
|
40
|
+
args = [a for a in args if a not in ("--save", "--show")]
|
|
41
|
+
|
|
42
|
+
config, _ = load_config()
|
|
43
|
+
panel, judge = default_panel(config)
|
|
44
|
+
|
|
45
|
+
if show:
|
|
46
|
+
print(f"\n Fusion panel — {describe_panel(panel, judge)}")
|
|
47
|
+
if not config.fusion.panel:
|
|
48
|
+
print(" (default built from the machine; run --save to persist)")
|
|
49
|
+
print()
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
if len(panel) < 2:
|
|
53
|
+
print(" ✗ No panel available — configure fusion.panel in "
|
|
54
|
+
"models.yaml (try /arka-fusion) or start Ollama with 2+ "
|
|
55
|
+
"models for a local panel.", file=sys.stderr)
|
|
19
56
|
return 1
|
|
20
|
-
|
|
57
|
+
|
|
58
|
+
if save:
|
|
59
|
+
where = _save_panel(panel, judge)
|
|
60
|
+
print(f" ✓ Fusion panel saved to {where}")
|
|
61
|
+
config, _ = load_config() # reload so fuse() uses the saved panel
|
|
62
|
+
|
|
63
|
+
prompt = " ".join(args).strip()
|
|
64
|
+
if not prompt:
|
|
65
|
+
print("usage: python -m core.fusion.cli [--save] \"question\"",
|
|
66
|
+
file=sys.stderr)
|
|
67
|
+
return 1
|
|
68
|
+
|
|
69
|
+
# Build an effective config carrying the (possibly default) panel.
|
|
70
|
+
config.fusion.panel = panel
|
|
71
|
+
config.fusion.judge = judge
|
|
21
72
|
try:
|
|
22
|
-
result = fuse(prompt)
|
|
73
|
+
result = fuse(prompt, config=config)
|
|
23
74
|
except FusionUnavailable as exc:
|
|
24
75
|
print(f" ✗ {exc}", file=sys.stderr)
|
|
25
76
|
return 1
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Build a fusion panel from what the machine can actually run.
|
|
2
|
+
|
|
3
|
+
Fusion needs a diverse panel + a judge. The user's ``models.yaml`` may
|
|
4
|
+
have an explicit ``fusion.panel`` (configured via ``/arka-fusion``); when
|
|
5
|
+
it is empty, this builds a sensible default from discovered models so
|
|
6
|
+
``npx arkaos fusion "question"`` does something out of the box instead of
|
|
7
|
+
failing with FusionUnavailable.
|
|
8
|
+
|
|
9
|
+
Diversity is the point (OpenRouter's result: a diverse panel beats a
|
|
10
|
+
single model): pair the runtime model with up to two large, distinct
|
|
11
|
+
local Ollama models. The judge stays the configured frontier model.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from core.runtime.model_router import ModelsConfig, RoleChoice, load_config
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def default_panel(
|
|
20
|
+
config: ModelsConfig | None = None,
|
|
21
|
+
) -> tuple[list[RoleChoice], RoleChoice]:
|
|
22
|
+
"""Return (panel, judge). Respects an explicit panel; else builds one.
|
|
23
|
+
|
|
24
|
+
An empty return panel means the machine offers nothing to fuse (no
|
|
25
|
+
local models and no explicit config) — the caller reports that rather
|
|
26
|
+
than fanning out to a single seat.
|
|
27
|
+
"""
|
|
28
|
+
if config is None:
|
|
29
|
+
config, _ = load_config()
|
|
30
|
+
judge = config.fusion.judge
|
|
31
|
+
if config.fusion.panel:
|
|
32
|
+
return list(config.fusion.panel), judge
|
|
33
|
+
|
|
34
|
+
panel: list[RoleChoice] = [
|
|
35
|
+
RoleChoice(provider="runtime", model="default", effort="high"),
|
|
36
|
+
]
|
|
37
|
+
try:
|
|
38
|
+
from core.runtime.ollama_discovery import discover
|
|
39
|
+
status = discover()
|
|
40
|
+
except Exception:
|
|
41
|
+
status = None
|
|
42
|
+
if status and status.running:
|
|
43
|
+
# Panel-grade locals: >= 4GB, or cloud-proxied (size 0). Sort by
|
|
44
|
+
# size desc so the strongest come first; take two distinct ones.
|
|
45
|
+
candidates = sorted(
|
|
46
|
+
(m for m in status.models if m.size_gb >= 4 or m.size_gb == 0),
|
|
47
|
+
key=lambda m: m.size_gb,
|
|
48
|
+
reverse=True,
|
|
49
|
+
)
|
|
50
|
+
for m in candidates[:2]:
|
|
51
|
+
panel.append(RoleChoice(provider="ollama", model=m.name, effort="high"))
|
|
52
|
+
return panel, judge
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def describe_panel(panel: list[RoleChoice], judge: RoleChoice) -> str:
|
|
56
|
+
"""Human summary of a panel for CLI output."""
|
|
57
|
+
seats = ", ".join(f"{c.provider}/{c.model}" for c in panel)
|
|
58
|
+
return f"judge {judge.provider}/{judge.model} | panel: {seats}"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -111,19 +111,29 @@ def _unlocked(fh) -> None:
|
|
|
111
111
|
# ─── Section 1: flow marker cache ────────────────────────────────────────
|
|
112
112
|
|
|
113
113
|
|
|
114
|
-
def
|
|
115
|
-
|
|
114
|
+
def _confirm_flow_authorization(session_id: str, transcript_path: str) -> None:
|
|
115
|
+
"""Confirm persistent flow authorization from the transcript.
|
|
116
|
+
|
|
117
|
+
Replaces the dead ``_write_flow_marker`` that read a non-existent
|
|
118
|
+
``assistant_message`` payload field. Reads the same transcript the
|
|
119
|
+
enforcer trusts and, when a flow marker is present, writes a confirmed
|
|
120
|
+
authorization that survives compaction and the 20-message window.
|
|
121
|
+
"""
|
|
122
|
+
if not session_id:
|
|
116
123
|
return
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
124
|
+
try:
|
|
125
|
+
from core.workflow.flow_authorization import confirm
|
|
126
|
+
from core.workflow.flow_enforcer import (
|
|
127
|
+
_load_last_assistant_messages,
|
|
128
|
+
_scan_markers,
|
|
129
|
+
)
|
|
130
|
+
except Exception:
|
|
123
131
|
return
|
|
124
132
|
try:
|
|
125
|
-
|
|
126
|
-
|
|
133
|
+
messages = _load_last_assistant_messages(transcript_path, 20)
|
|
134
|
+
marker_found, _ = _scan_markers(messages)
|
|
135
|
+
if marker_found is not None:
|
|
136
|
+
confirm(session_id, marker_found)
|
|
127
137
|
except Exception:
|
|
128
138
|
pass
|
|
129
139
|
|
|
@@ -614,9 +624,13 @@ def main(stdin_json: dict | None = None) -> int:
|
|
|
614
624
|
exit_code = get_str(stdin_json, "exit_code") or "0"
|
|
615
625
|
cwd = get_str(stdin_json, "cwd")
|
|
616
626
|
session_id = get_str(stdin_json, "session_id")
|
|
617
|
-
|
|
627
|
+
transcript_path = get_str(stdin_json, "transcript_path")
|
|
618
628
|
|
|
619
|
-
|
|
629
|
+
# Claude Code sends no `assistant_message` field (confirmed against the
|
|
630
|
+
# hook docs, 2026-07-05) — the old read was dead and the marker cache
|
|
631
|
+
# was never written. Scan the transcript (the only source hooks have)
|
|
632
|
+
# and confirm persistent authorization when a marker is present.
|
|
633
|
+
_confirm_flow_authorization(session_id, transcript_path)
|
|
620
634
|
|
|
621
635
|
if tool_name in ("Task", "Agent"):
|
|
622
636
|
subagent_type = get_str(stdin_json, "tool_input", "subagent_type")
|
|
@@ -260,6 +260,10 @@ def _flow_gate(
|
|
|
260
260
|
except Exception:
|
|
261
261
|
pass
|
|
262
262
|
if decision.allow:
|
|
263
|
+
# Grace path: allow, but surface the non-blocking warning so the
|
|
264
|
+
# operator sees the routing nudge instead of a silent pass.
|
|
265
|
+
if decision.warning:
|
|
266
|
+
print(decision.warning, file=sys.stderr)
|
|
263
267
|
return 0
|
|
264
268
|
return _deny(decision.to_stderr_message())
|
|
265
269
|
|
|
@@ -171,6 +171,13 @@ def _invalidate_turn_caches(session_id: str) -> None:
|
|
|
171
171
|
invalidate_marker(session_id)
|
|
172
172
|
except Exception:
|
|
173
173
|
pass
|
|
174
|
+
try:
|
|
175
|
+
# Reset only the per-turn grace flag; confirmed authorization and
|
|
176
|
+
# the grace counter persist across turns (enforcer resilience).
|
|
177
|
+
from core.workflow.flow_authorization import reset_turn
|
|
178
|
+
reset_turn(session_id)
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
174
181
|
try:
|
|
175
182
|
from core.synapse.kb_cache import invalidate_obsidian_query
|
|
176
183
|
invalidate_obsidian_query(session_id)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -33,6 +33,23 @@ USER_CONFIG_PATH = Path.home() / ".arkaos" / "models.yaml"
|
|
|
33
33
|
QUALITY_ROLES = frozenset({
|
|
34
34
|
"design", "review", "architecture", "strategy", "quality_gate",
|
|
35
35
|
})
|
|
36
|
+
|
|
37
|
+
# Plain-language explanation of each role, surfaced in the dashboard and
|
|
38
|
+
# `npx arkaos models` so the operator knows what "execution" etc. drive.
|
|
39
|
+
ROLE_DESCRIPTIONS: dict[str, str] = {
|
|
40
|
+
"design": "UI/UX and visual design — layout, typography, aesthetics.",
|
|
41
|
+
"review": "Code review, Quality Gate reviewers, adversarial verification.",
|
|
42
|
+
"architecture": "System design, API contracts, ADRs, technical decisions.",
|
|
43
|
+
"strategy": "Business and product strategy, market analysis, planning.",
|
|
44
|
+
"quality_gate": "The mandatory Quality Gate (Marta, Eduardo, Francisca).",
|
|
45
|
+
"execution": "Implementation — specialists writing code and running build tasks.",
|
|
46
|
+
"mechanical": "Rote work — commit messages, changelog, formatting, data fetch.",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def role_description(role: str) -> str:
|
|
51
|
+
"""Human-readable description of a role, or empty if unknown."""
|
|
52
|
+
return ROLE_DESCRIPTIONS.get(role, "")
|
|
36
53
|
KNOWN_EFFORTS = ("low", "medium", "high", "max")
|
|
37
54
|
|
|
38
55
|
_ALIAS_NAMES = frozenset({"best", "default", "fast"})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Model Fabric → orchestrator bridge (the consumption layer).
|
|
2
|
+
|
|
3
|
+
The dashboard and ``npx arkaos models`` write ``~/.arkaos/models.yaml``.
|
|
4
|
+
Until now nothing fed that config back to the Claude Code orchestrator,
|
|
5
|
+
so changing a role's model changed nothing at dispatch time. This module
|
|
6
|
+
turns the config into a compact directive the SessionStart hook injects,
|
|
7
|
+
so the orchestrator resolves an agent's model from the user's config
|
|
8
|
+
instead of only the agent YAML default.
|
|
9
|
+
|
|
10
|
+
It is context injection — the same control surface ArkaOS uses for the
|
|
11
|
+
constitution, routing, and evidence flow — not a hard rewrite of the Task
|
|
12
|
+
`model` param (Claude Code exposes no hook for that). The PostToolUse
|
|
13
|
+
telemetry already records the model actually requested per dispatch, so
|
|
14
|
+
mismatches are observable (see model_routing_check).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
# Which Model Fabric role governs each kind of agent work. Agents are a
|
|
20
|
+
# different taxonomy from roles, so we map by what the agent DOES. The
|
|
21
|
+
# orchestrator uses this to pick the role, then resolves the model.
|
|
22
|
+
AGENT_ROLE_HINTS: dict[str, str] = {
|
|
23
|
+
# quality-critical
|
|
24
|
+
"architect": "architecture",
|
|
25
|
+
"cto": "architecture",
|
|
26
|
+
"strategist": "strategy",
|
|
27
|
+
"brand-strategist": "strategy",
|
|
28
|
+
"creative-director": "design",
|
|
29
|
+
"visual-designer": "design",
|
|
30
|
+
"frontend-dev": "design",
|
|
31
|
+
"marta-cqo": "quality_gate",
|
|
32
|
+
"eduardo-copy": "quality_gate",
|
|
33
|
+
"francisca-tech": "quality_gate",
|
|
34
|
+
"code-reviewer": "review",
|
|
35
|
+
"security": "review",
|
|
36
|
+
"qa": "review",
|
|
37
|
+
# execution
|
|
38
|
+
"senior-dev": "execution",
|
|
39
|
+
"paulo-tech-lead": "execution",
|
|
40
|
+
"devops": "execution",
|
|
41
|
+
# mechanical
|
|
42
|
+
"analyst": "mechanical",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def routing_summary() -> str:
|
|
47
|
+
"""One-line role→provider/model@effort summary, or '' if unavailable."""
|
|
48
|
+
try:
|
|
49
|
+
from core.runtime.model_router import resolve_all
|
|
50
|
+
items = resolve_all()
|
|
51
|
+
except Exception:
|
|
52
|
+
return ""
|
|
53
|
+
parts = [
|
|
54
|
+
f"{i.role}={i.provider}/{i.model or '?'}@{i.effort}" for i in items
|
|
55
|
+
]
|
|
56
|
+
return " ".join(parts)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def routing_directive() -> str:
|
|
60
|
+
"""Full SessionStart block: the config plus how to honor it.
|
|
61
|
+
|
|
62
|
+
Returns '' when the config cannot be read, so the hook can skip the
|
|
63
|
+
block entirely rather than inject an empty directive.
|
|
64
|
+
"""
|
|
65
|
+
summary = routing_summary()
|
|
66
|
+
if not summary:
|
|
67
|
+
return ""
|
|
68
|
+
# No backticks: this string is embedded in session-start.sh which
|
|
69
|
+
# passes it through $(echo -e "$MSG") — backticks would trigger shell
|
|
70
|
+
# command substitution. Use plain quotes.
|
|
71
|
+
return (
|
|
72
|
+
"[ARKA:MODEL-FABRIC] The operator's model routing (from "
|
|
73
|
+
"~/.arkaos/models.yaml — dashboard / npx arkaos models):\n"
|
|
74
|
+
f" {summary}\n"
|
|
75
|
+
"When dispatching a subagent via the Task tool, set its model "
|
|
76
|
+
"param from this table by the KIND of work it does (design, "
|
|
77
|
+
"review, architecture, strategy, quality_gate, execution, "
|
|
78
|
+
"mechanical) — this is the user's explicit choice and OVERRIDES "
|
|
79
|
+
"the agent YAML default. Quality roles never downgrade below the "
|
|
80
|
+
"configured model (excellence-mandate). Genuinely mechanical "
|
|
81
|
+
"dispatches (commit messages, formatting) use the mechanical role."
|
|
82
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Known models per runtime, for the Model Fabric UI + advisor.
|
|
2
|
+
|
|
3
|
+
The Model Fabric ``runtime`` provider shells out to the active CLI. This
|
|
4
|
+
module lists the concrete models that CLI accepts, with human labels and
|
|
5
|
+
tier, so the dashboard dropdown and ``/arka-fusion`` can offer real
|
|
6
|
+
choices (Fable 5, Opus, Sonnet, Haiku) instead of only the abstract
|
|
7
|
+
best/default/fast aliases.
|
|
8
|
+
|
|
9
|
+
Model IDs verified against the Claude API reference (2026-07). This is
|
|
10
|
+
the single place they are hand-listed — update HERE when the runtime
|
|
11
|
+
ships new models; the dashboard and CLI read from this module. Codex /
|
|
12
|
+
Gemini / Cursor models are left empty on purpose: we do not hardcode
|
|
13
|
+
another vendor's catalogue, and the UI falls back to a free-text field
|
|
14
|
+
so the user types the exact id their runtime accepts.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Claude Code accepts the short aliases opus/sonnet/haiku and full model
|
|
21
|
+
# IDs. `value` is what gets written to models.yaml as the role's model.
|
|
22
|
+
CLAUDE_CODE_MODELS: list[dict[str, str]] = [
|
|
23
|
+
{"value": "claude-fable-5", "label": "Fable 5", "tier": "frontier",
|
|
24
|
+
"note": "most capable"},
|
|
25
|
+
{"value": "opus", "label": "Opus 4.8", "tier": "frontier",
|
|
26
|
+
"note": "frontier"},
|
|
27
|
+
{"value": "sonnet", "label": "Sonnet 5", "tier": "balanced",
|
|
28
|
+
"note": "balanced speed/quality"},
|
|
29
|
+
{"value": "haiku", "label": "Haiku 4.5", "tier": "fast",
|
|
30
|
+
"note": "fast + cheap"},
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
_MODELS_BY_RUNTIME: dict[str, list[dict[str, str]]] = {
|
|
34
|
+
"claude-code": CLAUDE_CODE_MODELS,
|
|
35
|
+
# codex / gemini / cursor intentionally omitted — free-text in the UI.
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def models_for_runtime(runtime_id: str) -> list[dict[str, str]]:
|
|
40
|
+
"""Return the known runtime models for the given runtime id."""
|
|
41
|
+
return _MODELS_BY_RUNTIME.get(runtime_id, [])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def detect_runtime_models() -> tuple[str, list[dict[str, str]]]:
|
|
45
|
+
"""Best-effort (runtime_id, models) for the active runtime."""
|
|
46
|
+
try:
|
|
47
|
+
from core.runtime.registry import detect_runtime
|
|
48
|
+
runtime_id = detect_runtime()
|
|
49
|
+
except Exception:
|
|
50
|
+
runtime_id = "claude-code"
|
|
51
|
+
return runtime_id, models_for_runtime(runtime_id)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|