arkaos 5.10.0 → 5.11.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/bin/arka-menubar.py +12 -5
- package/core/agents/loader.py +1 -1
- package/core/agents/registry_gen.py +1 -1
- package/core/budget/manager.py +2 -2
- package/core/cognition/scheduler/daemon.py +2 -2
- package/core/conclave/persistence.py +2 -2
- package/core/forge/orchestrator.py +5 -1
- package/core/governance/constitution.py +1 -1
- package/core/governance/quality_api.py +4 -4
- package/core/governance/skill_proposer.py +82 -5
- package/core/hooks/session_start.py +78 -3
- package/core/keys.py +2 -2
- package/core/obsidian/writer.py +1 -1
- package/core/personas/manager.py +3 -3
- package/core/specs/manager.py +2 -2
- package/core/squads/loader.py +1 -1
- package/core/synapse/kb_cache.py +2 -2
- package/core/tasks/manager.py +2 -2
- package/core/workflow/loader.py +1 -1
- package/dashboard/app/assets/css/main.css +0 -29
- package/dashboard/app/composables/useApi.ts +33 -2
- package/dashboard/nuxt.config.ts +0 -7
- package/departments/dev/skills/animated-website/scripts/extract_frames.py +1 -1
- package/departments/dev/skills/onboard/scripts/detect-stack.py +5 -5
- package/knowledge/skills-manifest.json +1 -1
- package/mcps/arka-prompts/server.py +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/dashboard-api.py +4 -4
- package/scripts/harness_gen.py +4 -4
- package/scripts/synapse-bridge.py +2 -2
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.
|
|
1
|
+
5.11.0
|
package/bin/arka-menubar.py
CHANGED
|
@@ -79,17 +79,23 @@ def read_state(home: Path | None = None) -> dict:
|
|
|
79
79
|
"autoupdate_on": True,
|
|
80
80
|
}
|
|
81
81
|
try:
|
|
82
|
-
manifest = json.loads(
|
|
82
|
+
manifest = json.loads(
|
|
83
|
+
(home / "install-manifest.json").read_text(encoding="utf-8")
|
|
84
|
+
)
|
|
83
85
|
state["version"] = manifest.get("version") or None
|
|
84
86
|
except Exception:
|
|
85
87
|
pass
|
|
86
88
|
try:
|
|
87
|
-
sync = json.loads(
|
|
89
|
+
sync = json.loads(
|
|
90
|
+
(home / "sync-state.json").read_text(encoding="utf-8")
|
|
91
|
+
)
|
|
88
92
|
state["sync_pending"] = sync.get("version") == "pending-sync"
|
|
89
93
|
except Exception:
|
|
90
94
|
pass
|
|
91
95
|
try:
|
|
92
|
-
profile = json.loads(
|
|
96
|
+
profile = json.loads(
|
|
97
|
+
(home / "profile.json").read_text(encoding="utf-8")
|
|
98
|
+
)
|
|
93
99
|
value = str(profile.get("installProfile", "essential")).strip().lower()
|
|
94
100
|
state["profile"] = value if value in VALID_PROFILES else "essential"
|
|
95
101
|
except Exception:
|
|
@@ -154,7 +160,7 @@ def stable_script(name: str) -> Path | None:
|
|
|
154
160
|
if lib.exists():
|
|
155
161
|
return lib
|
|
156
162
|
try:
|
|
157
|
-
repo = Path((home / ".repo-path").read_text().strip())
|
|
163
|
+
repo = Path((home / ".repo-path").read_text(encoding="utf-8").strip())
|
|
158
164
|
candidate = repo / "scripts" / name
|
|
159
165
|
if candidate.exists():
|
|
160
166
|
return candidate
|
|
@@ -190,7 +196,8 @@ def action_open_dashboard() -> None:
|
|
|
190
196
|
log_line(f"open_dashboard: ensure failed ({err})")
|
|
191
197
|
ui_port = ""
|
|
192
198
|
try:
|
|
193
|
-
|
|
199
|
+
ports = (arka_home() / "dashboard.ports").read_text(encoding="utf-8")
|
|
200
|
+
for line in ports.splitlines():
|
|
194
201
|
if line.startswith("UI_PORT="):
|
|
195
202
|
ui_port = line.split("=", 1)[1].strip()
|
|
196
203
|
except Exception:
|
package/core/agents/loader.py
CHANGED
|
@@ -126,7 +126,7 @@ def generate_registry(departments_dir: str | Path, output_path: str | Path) -> d
|
|
|
126
126
|
registry["_meta"]["errors"] = errors
|
|
127
127
|
|
|
128
128
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
129
|
-
with open(output_path, "w") as f:
|
|
129
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
130
130
|
json.dump(registry, f, indent=2, ensure_ascii=False)
|
|
131
131
|
|
|
132
132
|
return registry
|
package/core/budget/manager.py
CHANGED
|
@@ -178,13 +178,13 @@ class BudgetManager:
|
|
|
178
178
|
"counter": self._counter,
|
|
179
179
|
"usages": [u.model_dump(mode="json") for u in self._usages],
|
|
180
180
|
}
|
|
181
|
-
with open(self._storage_path, "w") as f:
|
|
181
|
+
with open(self._storage_path, "w", encoding="utf-8") as f:
|
|
182
182
|
json.dump(data, f, indent=2)
|
|
183
183
|
|
|
184
184
|
def _load(self) -> None:
|
|
185
185
|
if self._storage_path is None or not self._storage_path.exists():
|
|
186
186
|
return
|
|
187
|
-
content = self._storage_path.read_text().strip()
|
|
187
|
+
content = self._storage_path.read_text(encoding="utf-8").strip()
|
|
188
188
|
if not content:
|
|
189
189
|
return
|
|
190
190
|
data = json.loads(content)
|
|
@@ -61,7 +61,7 @@ class ScheduleConfig:
|
|
|
61
61
|
@classmethod
|
|
62
62
|
def load(cls, config_path: str) -> "list[ScheduleConfig]":
|
|
63
63
|
"""Load schedules from YAML, returning only enabled entries."""
|
|
64
|
-
with open(config_path) as fh:
|
|
64
|
+
with open(config_path, encoding="utf-8") as fh:
|
|
65
65
|
data = yaml.safe_load(fh)
|
|
66
66
|
|
|
67
67
|
schedules = []
|
|
@@ -106,7 +106,7 @@ class ArkaScheduler:
|
|
|
106
106
|
"""Acquire an exclusive file lock. Returns False if already locked."""
|
|
107
107
|
Path(self._lock_path).parent.mkdir(parents=True, exist_ok=True)
|
|
108
108
|
try:
|
|
109
|
-
fd = open(self._lock_path, "w") # noqa: WPS515
|
|
109
|
+
fd = open(self._lock_path, "w", encoding="utf-8") # noqa: WPS515
|
|
110
110
|
if sys.platform == "win32":
|
|
111
111
|
import msvcrt # type: ignore[import]
|
|
112
112
|
|
|
@@ -22,7 +22,7 @@ def save_profile(board: ConclaveBoard, path: str | Path = "") -> None:
|
|
|
22
22
|
"contrarian": [a.model_dump(mode="json") for a in board.contrarian],
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
with open(path, "w") as f:
|
|
25
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
26
26
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
27
27
|
|
|
28
28
|
|
|
@@ -33,7 +33,7 @@ def load_profile(path: str | Path = "") -> Optional[ConclaveBoard]:
|
|
|
33
33
|
if not path.exists():
|
|
34
34
|
return None
|
|
35
35
|
|
|
36
|
-
content = path.read_text().strip()
|
|
36
|
+
content = path.read_text(encoding="utf-8").strip()
|
|
37
37
|
if not content:
|
|
38
38
|
return None
|
|
39
39
|
|
|
@@ -474,7 +474,11 @@ class ForgeOrchestrator:
|
|
|
474
474
|
repo = Path.cwd().name
|
|
475
475
|
|
|
476
476
|
version_file = Path(__file__).parent.parent.parent / "VERSION"
|
|
477
|
-
version =
|
|
477
|
+
version = (
|
|
478
|
+
version_file.read_text(encoding="utf-8").strip()
|
|
479
|
+
if version_file.exists()
|
|
480
|
+
else "unknown"
|
|
481
|
+
)
|
|
478
482
|
|
|
479
483
|
self._forge_context = ForgeContext(
|
|
480
484
|
repo=repo,
|
|
@@ -146,7 +146,7 @@ def load_constitution(path: str | Path) -> Constitution:
|
|
|
146
146
|
if not path.exists():
|
|
147
147
|
raise FileNotFoundError(f"Constitution file not found: {path}")
|
|
148
148
|
|
|
149
|
-
with open(path) as f:
|
|
149
|
+
with open(path, encoding="utf-8") as f:
|
|
150
150
|
data = yaml.safe_load(f)
|
|
151
151
|
|
|
152
152
|
return Constitution.model_validate(data)
|
|
@@ -31,14 +31,14 @@ def _load_queue() -> list[dict]:
|
|
|
31
31
|
if not _QUEUE_FILE.exists():
|
|
32
32
|
return []
|
|
33
33
|
try:
|
|
34
|
-
return json.loads(_QUEUE_FILE.read_text())
|
|
34
|
+
return json.loads(_QUEUE_FILE.read_text(encoding="utf-8"))
|
|
35
35
|
except (json.JSONDecodeError, OSError):
|
|
36
36
|
return []
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def _save_queue(queue: list[dict]) -> None:
|
|
40
40
|
_ensure_state_dir()
|
|
41
|
-
_QUEUE_FILE.write_text(json.dumps(queue, indent=2))
|
|
41
|
+
_QUEUE_FILE.write_text(json.dumps(queue, indent=2), encoding="utf-8")
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
def _load_workflows() -> list[dict]:
|
|
@@ -46,14 +46,14 @@ def _load_workflows() -> list[dict]:
|
|
|
46
46
|
if not _WORKFLOWS_FILE.exists():
|
|
47
47
|
return []
|
|
48
48
|
try:
|
|
49
|
-
return json.loads(_WORKFLOWS_FILE.read_text())
|
|
49
|
+
return json.loads(_WORKFLOWS_FILE.read_text(encoding="utf-8"))
|
|
50
50
|
except (json.JSONDecodeError, OSError):
|
|
51
51
|
return []
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def _save_workflows(workflows: list[dict]) -> None:
|
|
55
55
|
_ensure_state_dir()
|
|
56
|
-
_WORKFLOWS_FILE.write_text(json.dumps(workflows, indent=2))
|
|
56
|
+
_WORKFLOWS_FILE.write_text(json.dumps(workflows, indent=2), encoding="utf-8")
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def submit(
|
|
@@ -9,9 +9,10 @@ Mirror of the PR20 reorganizer pattern but focused on capability-capture.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import hashlib
|
|
12
13
|
import re
|
|
13
14
|
from dataclasses import dataclass
|
|
14
|
-
from datetime import
|
|
15
|
+
from datetime import UTC, datetime
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
17
18
|
_COMPLETION_PATTERNS: tuple[re.Pattern[str], ...] = (
|
|
@@ -41,7 +42,14 @@ _DEFAULT_OUTPUT_DIR: Path = Path.home() / ".arkaos" / "skill-proposals"
|
|
|
41
42
|
|
|
42
43
|
@dataclass(frozen=True)
|
|
43
44
|
class SkillProposal:
|
|
44
|
-
"""Outcome of a skill-evaluation pass.
|
|
45
|
+
"""Outcome of a skill-evaluation pass.
|
|
46
|
+
|
|
47
|
+
``proposal_path`` is set only when a file was actually written. It is
|
|
48
|
+
``None`` both when no proposal was warranted and when one was
|
|
49
|
+
rendered but had nowhere safe to go (reason ``no-safe-filename``) —
|
|
50
|
+
in that second case ``proposal_markdown`` still carries the capture,
|
|
51
|
+
so a caller can route it somewhere else.
|
|
52
|
+
"""
|
|
45
53
|
should_propose: bool
|
|
46
54
|
reason: str
|
|
47
55
|
suggested_slug: str | None
|
|
@@ -75,12 +83,81 @@ def evaluate(
|
|
|
75
83
|
markdown = _render_proposal(text, slug, today=today)
|
|
76
84
|
out_dir = output_dir or _DEFAULT_OUTPUT_DIR
|
|
77
85
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
78
|
-
iso_today = today or datetime.now(
|
|
79
|
-
path = out_dir
|
|
86
|
+
iso_today = today or datetime.now(UTC).strftime("%Y-%m-%d")
|
|
87
|
+
path = _collision_free_path(out_dir, iso_today, slug, markdown)
|
|
88
|
+
if path is None:
|
|
89
|
+
return SkillProposal(False, "no-safe-filename", slug, None, markdown)
|
|
80
90
|
path.write_text(markdown, encoding="utf-8")
|
|
81
91
|
return SkillProposal(True, "proposed", slug, path, markdown)
|
|
82
92
|
|
|
83
93
|
|
|
94
|
+
def _collision_free_path(
|
|
95
|
+
out_dir: Path, iso_today: str, slug: str, markdown: str
|
|
96
|
+
) -> Path | None:
|
|
97
|
+
"""Return a path for today's proposal, or ``None`` if none is safe.
|
|
98
|
+
|
|
99
|
+
``_suggest_slug`` anchors on the first matching skill-worthy hint, so
|
|
100
|
+
same-day slug collisions are the norm: the six word hints plus the
|
|
101
|
+
fallback yield seven fixed names, and the numeric ``N-phase`` hint
|
|
102
|
+
mints a fresh one per number it matches (until ``_slugify``'s 60-char
|
|
103
|
+
cap truncates the extremes back together). The space is small in
|
|
104
|
+
practice, but it is not the fixed handful this docstring once
|
|
105
|
+
claimed. Every proposal after the first with the same slug used to
|
|
106
|
+
overwrite its predecessor, silently: distinct captured capabilities
|
|
107
|
+
were lost with no error and no trace.
|
|
108
|
+
|
|
109
|
+
Disambiguates by content digest rather than a counter, so re-running
|
|
110
|
+
the hook over the same closing message stays idempotent (same content
|
|
111
|
+
-> same path -> one file) while genuinely different proposals get
|
|
112
|
+
their own. One invariant holds every branch honest: never return a
|
|
113
|
+
path unless it is free or provably holds this exact proposal.
|
|
114
|
+
|
|
115
|
+
A name built from our digest proves nothing about the bytes inside
|
|
116
|
+
the file — any file can carry any name, no hash collision required.
|
|
117
|
+
So when the plain name and both digest names are all occupied by
|
|
118
|
+
content we cannot account for, this returns ``None`` and the caller
|
|
119
|
+
writes nothing: losing one capture is honest, overwriting somebody
|
|
120
|
+
else's is not.
|
|
121
|
+
|
|
122
|
+
The digest names are checked before the plain one, so a re-fire after
|
|
123
|
+
the operator deleted the plain twin lands back on the file it already
|
|
124
|
+
wrote instead of duplicating it.
|
|
125
|
+
"""
|
|
126
|
+
digest = hashlib.sha256(markdown.encode("utf-8")).hexdigest()
|
|
127
|
+
plain = out_dir / f"{iso_today}-{slug}.md"
|
|
128
|
+
# 8 hex chars keep the filename readable; the full digest is the
|
|
129
|
+
# tie-breaker for the day those 32 bits meet a different proposal.
|
|
130
|
+
digest_paths = (
|
|
131
|
+
out_dir / f"{iso_today}-{slug}-{digest[:8]}.md",
|
|
132
|
+
out_dir / f"{iso_today}-{slug}-{digest}.md",
|
|
133
|
+
)
|
|
134
|
+
for candidate in digest_paths:
|
|
135
|
+
if _already_holds(candidate, markdown):
|
|
136
|
+
return candidate
|
|
137
|
+
if not plain.exists() or _already_holds(plain, markdown):
|
|
138
|
+
return plain
|
|
139
|
+
for candidate in digest_paths:
|
|
140
|
+
if not candidate.exists():
|
|
141
|
+
return candidate
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _already_holds(path: Path, markdown: str) -> bool:
|
|
146
|
+
"""True only when ``path`` provably contains exactly ``markdown``.
|
|
147
|
+
|
|
148
|
+
Content we cannot read back is not "equal": a missing, unreadable, or
|
|
149
|
+
non-UTF-8 file is unknown content, and the caller must treat unknown
|
|
150
|
+
as another proposal rather than write over it. ``UnicodeDecodeError``
|
|
151
|
+
is a ``ValueError``, not an ``OSError`` — both belong in the same
|
|
152
|
+
branch, or the exception escapes ``evaluate`` into the Stop hook's
|
|
153
|
+
blanket ``except Exception: pass`` and the proposal is lost silently.
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
return path.read_text(encoding="utf-8") == markdown
|
|
157
|
+
except (OSError, ValueError):
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
|
|
84
161
|
def _has_completion_signal(text: str) -> bool:
|
|
85
162
|
return any(p.search(text) for p in _COMPLETION_PATTERNS)
|
|
86
163
|
|
|
@@ -108,7 +185,7 @@ def _slugify(value: str) -> str:
|
|
|
108
185
|
|
|
109
186
|
|
|
110
187
|
def _render_proposal(text: str, slug: str, *, today: str | None) -> str:
|
|
111
|
-
iso = today or datetime.now(
|
|
188
|
+
iso = today or datetime.now(UTC).strftime("%Y-%m-%d")
|
|
112
189
|
excerpt = text.strip()
|
|
113
190
|
if len(excerpt) > 1000:
|
|
114
191
|
excerpt = excerpt[:1000].rstrip() + "..."
|
|
@@ -24,6 +24,7 @@ module; with no usable venv it emits a static banner (fail-open).
|
|
|
24
24
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
|
+
import contextlib
|
|
27
28
|
import json
|
|
28
29
|
import os
|
|
29
30
|
import re
|
|
@@ -247,13 +248,46 @@ def _marker_safe(value: str) -> str:
|
|
|
247
248
|
return _CONTROL_RE.sub("", value.splitlines()[0]) if value else value
|
|
248
249
|
|
|
249
250
|
|
|
251
|
+
def _spawning_suppressed() -> bool:
|
|
252
|
+
"""Whether this process may launch the operator's background daemons.
|
|
253
|
+
|
|
254
|
+
SessionStart spawns two long-lived side processes: the dashboard
|
|
255
|
+
(`_ensure_dashboard`) and the reorganizer. Both take the *resolved repo
|
|
256
|
+
root* as their working tree, and `start-dashboard` kills the PIDs
|
|
257
|
+
recorded in `~/.arkaos/dashboard.pid` — one shared file for every
|
|
258
|
+
checkout on the machine — before registering its own. (`ensure` leaves
|
|
259
|
+
a healthy instance alone; `find_port` steps past occupied ports.)
|
|
260
|
+
|
|
261
|
+
Run the hook from a test and that is a live-fire action on the machine
|
|
262
|
+
running the tests: the operator's dashboard is killed through that
|
|
263
|
+
global PID file and replaced by one served out of the test tree — a
|
|
264
|
+
checkout that legitimately lacks whatever local state the real install
|
|
265
|
+
has. It looks like the dashboard broke, not like a test ran.
|
|
266
|
+
(Observed 2026-07-27: two separate pytest invocations silently took
|
|
267
|
+
over ports 3333/3334 and served a blank UI.)
|
|
268
|
+
|
|
269
|
+
ARKA_HOOK_NO_SPAWN=1 is the explicit switch. PYTEST_CURRENT_TEST is
|
|
270
|
+
honored as well, deliberately: a test author who forgets the switch
|
|
271
|
+
should lose a background process they never wanted, not the dashboard
|
|
272
|
+
they were using. pytest exports it into the environment every child
|
|
273
|
+
process inherits, so the safety net costs nothing in production, where
|
|
274
|
+
the variable is simply absent.
|
|
275
|
+
"""
|
|
276
|
+
return bool(
|
|
277
|
+
os.environ.get("ARKA_HOOK_NO_SPAWN") == "1"
|
|
278
|
+
or os.environ.get("PYTEST_CURRENT_TEST")
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
250
282
|
def _spawn_detached(cmd: list[str], repo: str, log_path: Path | None = None) -> None:
|
|
283
|
+
if _spawning_suppressed():
|
|
284
|
+
return
|
|
251
285
|
stdout = subprocess.DEVNULL
|
|
252
286
|
handle = None
|
|
253
287
|
try:
|
|
254
288
|
if log_path is not None:
|
|
255
289
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
256
|
-
handle = log_path.open("a")
|
|
290
|
+
handle = log_path.open("a", encoding="utf-8")
|
|
257
291
|
stdout = handle
|
|
258
292
|
subprocess.Popen(
|
|
259
293
|
cmd,
|
|
@@ -436,6 +470,47 @@ def build_recap(cwd: str, budget_ms: int = _BUDGET_MS) -> str:
|
|
|
436
470
|
return ""
|
|
437
471
|
|
|
438
472
|
|
|
473
|
+
_CONTRACT_FAILURE_LOG = (
|
|
474
|
+
Path.home() / ".arkaos" / "telemetry" / "session-start-failures.jsonl"
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _contracts_unavailable_notice(exc: BaseException) -> str:
|
|
479
|
+
"""Say the contracts are missing instead of returning an empty string.
|
|
480
|
+
|
|
481
|
+
build_context() produces [ARKA:EVIDENCE-FLOW], [ARKA:SKILL-CONTRACT],
|
|
482
|
+
[ARKA:META-TAG], [ARKA:AUTHORITY] and [ARKA:MODEL-FABRIC], plus the
|
|
483
|
+
resume and root lines and, when present, the [SESSION-MEMORY] recap —
|
|
484
|
+
the rules the session is supposed to run under. Swallowing a failure here returns the exact
|
|
485
|
+
state those blocks exist to prevent: a session with no idea the rules
|
|
486
|
+
exist, behaving like a generic assistant, with nothing anywhere saying
|
|
487
|
+
why. That is indistinguishable from the hook never having run, which
|
|
488
|
+
is precisely how the Windows delivery gap survived unnoticed for
|
|
489
|
+
months (PR #408).
|
|
490
|
+
|
|
491
|
+
Same principle `_authority_brief` already applies: one honest line
|
|
492
|
+
beats a silent void. The greeting still never breaks
|
|
493
|
+
and the hook still exits 0.
|
|
494
|
+
"""
|
|
495
|
+
with contextlib.suppress(Exception): # telemetry must never be the thing that breaks
|
|
496
|
+
_CONTRACT_FAILURE_LOG.parent.mkdir(parents=True, exist_ok=True)
|
|
497
|
+
with _CONTRACT_FAILURE_LOG.open("a", encoding="utf-8") as fh:
|
|
498
|
+
fh.write(json.dumps({
|
|
499
|
+
"ts": datetime.now(UTC).isoformat(),
|
|
500
|
+
"event": "build_context_failed",
|
|
501
|
+
"error_type": type(exc).__name__,
|
|
502
|
+
"error": str(exc)[:500],
|
|
503
|
+
}) + "\n")
|
|
504
|
+
return (
|
|
505
|
+
f"\n[ARKA:CONTRACTS] unavailable ({type(exc).__name__}) — the evidence "
|
|
506
|
+
f"flow, skill-routing, meta-tag, authority and model-routing contracts "
|
|
507
|
+
f"could not be built for this session. Treat their absence as a fault, not as "
|
|
508
|
+
f"permission: keep routing and gating as if they were present, and "
|
|
509
|
+
f"report the failure. Detail in "
|
|
510
|
+
f"~/.arkaos/telemetry/session-start-failures.jsonl"
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
|
|
439
514
|
def main(stdin_json: dict | None = None) -> int:
|
|
440
515
|
if stdin_json is None:
|
|
441
516
|
stdin_json, _ = read_stdin_json()
|
|
@@ -450,8 +525,8 @@ def main(stdin_json: dict | None = None) -> int:
|
|
|
450
525
|
visible = _FALLBACK_BANNER + "\n Olá, founder\n"
|
|
451
526
|
try:
|
|
452
527
|
context = build_context(cwd)
|
|
453
|
-
except Exception: #
|
|
454
|
-
context =
|
|
528
|
+
except Exception as exc: # greeting never breaks — but never lie either
|
|
529
|
+
context = _contracts_unavailable_notice(exc)
|
|
455
530
|
payload: dict = {"systemMessage": visible}
|
|
456
531
|
if context:
|
|
457
532
|
# Assign the sub-dict key explicitly — SessionStart's wrapper also
|
package/core/keys.py
CHANGED
|
@@ -34,12 +34,12 @@ PROVIDERS = {
|
|
|
34
34
|
def _load() -> dict[str, str]:
|
|
35
35
|
if not KEYS_PATH.exists():
|
|
36
36
|
return {}
|
|
37
|
-
return json.loads(KEYS_PATH.read_text())
|
|
37
|
+
return json.loads(KEYS_PATH.read_text(encoding="utf-8"))
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def _save(keys: dict[str, str]) -> None:
|
|
41
41
|
KEYS_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
-
KEYS_PATH.write_text(json.dumps(keys, indent=2))
|
|
42
|
+
KEYS_PATH.write_text(json.dumps(keys, indent=2), encoding="utf-8")
|
|
43
43
|
os.chmod(KEYS_PATH, stat.S_IRUSR | stat.S_IWUSR) # 600
|
|
44
44
|
|
|
45
45
|
|
package/core/obsidian/writer.py
CHANGED
|
@@ -131,7 +131,7 @@ class ObsidianWriter:
|
|
|
131
131
|
if config_path.exists():
|
|
132
132
|
try:
|
|
133
133
|
from core.runtime.path_resolver import resolve
|
|
134
|
-
config = json.loads(config_path.read_text())
|
|
134
|
+
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
135
135
|
vault = resolve(config.get("vault_path", ""))
|
|
136
136
|
if vault and not vault.startswith("${") and Path(vault).exists():
|
|
137
137
|
return Path(vault)
|
package/core/personas/manager.py
CHANGED
|
@@ -73,7 +73,7 @@ class PersonaManager:
|
|
|
73
73
|
output_dir = Path(agents_dir)
|
|
74
74
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
75
75
|
output_path = output_dir / f"{agent_id}.yaml"
|
|
76
|
-
with open(output_path, "w") as f:
|
|
76
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
77
77
|
yaml.dump(agent_data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
78
78
|
|
|
79
79
|
# Track the clone
|
|
@@ -88,13 +88,13 @@ class PersonaManager:
|
|
|
88
88
|
return
|
|
89
89
|
self._storage_path.parent.mkdir(parents=True, exist_ok=True)
|
|
90
90
|
data = {pid: p.model_dump(mode="json") for pid, p in self._personas.items()}
|
|
91
|
-
with open(self._storage_path, "w") as f:
|
|
91
|
+
with open(self._storage_path, "w", encoding="utf-8") as f:
|
|
92
92
|
json.dump(data, f, indent=2)
|
|
93
93
|
|
|
94
94
|
def _load(self) -> None:
|
|
95
95
|
if self._storage_path is None or not self._storage_path.exists():
|
|
96
96
|
return
|
|
97
|
-
content = self._storage_path.read_text().strip()
|
|
97
|
+
content = self._storage_path.read_text(encoding="utf-8").strip()
|
|
98
98
|
if not content:
|
|
99
99
|
return
|
|
100
100
|
data = json.loads(content)
|
package/core/specs/manager.py
CHANGED
|
@@ -108,7 +108,7 @@ class SpecManager:
|
|
|
108
108
|
path = Path(path)
|
|
109
109
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
110
110
|
data = spec.model_dump(mode="json")
|
|
111
|
-
with open(path, "w") as f:
|
|
111
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
112
112
|
yaml.dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
113
113
|
return True
|
|
114
114
|
|
|
@@ -117,7 +117,7 @@ class SpecManager:
|
|
|
117
117
|
path = Path(path)
|
|
118
118
|
if not path.exists():
|
|
119
119
|
raise FileNotFoundError(f"Spec file not found: {path}")
|
|
120
|
-
with open(path) as f:
|
|
120
|
+
with open(path, encoding="utf-8") as f:
|
|
121
121
|
data = yaml.safe_load(f)
|
|
122
122
|
spec = Spec.model_validate(data)
|
|
123
123
|
self._specs[spec.id] = spec
|
package/core/squads/loader.py
CHANGED
package/core/synapse/kb_cache.py
CHANGED
|
@@ -179,12 +179,12 @@ class KBSessionCache:
|
|
|
179
179
|
if not self._cache_file.exists():
|
|
180
180
|
return {}
|
|
181
181
|
try:
|
|
182
|
-
return json.loads(self._cache_file.read_text())
|
|
182
|
+
return json.loads(self._cache_file.read_text(encoding="utf-8"))
|
|
183
183
|
except (json.JSONDecodeError, OSError):
|
|
184
184
|
return {}
|
|
185
185
|
|
|
186
186
|
def _save(self, data: dict[str, Any]) -> None:
|
|
187
|
-
self._cache_file.write_text(json.dumps(data, indent=2))
|
|
187
|
+
self._cache_file.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
188
188
|
|
|
189
189
|
def extract_topics(self, query: str) -> set[str]:
|
|
190
190
|
"""Extract key topics from a query string.
|
package/core/tasks/manager.py
CHANGED
|
@@ -135,13 +135,13 @@ class TaskManager:
|
|
|
135
135
|
"counter": self._counter,
|
|
136
136
|
"tasks": {tid: t.model_dump(mode="json") for tid, t in self._tasks.items()},
|
|
137
137
|
}
|
|
138
|
-
with open(self._storage_path, "w") as f:
|
|
138
|
+
with open(self._storage_path, "w", encoding="utf-8") as f:
|
|
139
139
|
json.dump(data, f, indent=2)
|
|
140
140
|
|
|
141
141
|
def _load(self) -> None:
|
|
142
142
|
if self._storage_path is None or not self._storage_path.exists():
|
|
143
143
|
return
|
|
144
|
-
content = self._storage_path.read_text().strip()
|
|
144
|
+
content = self._storage_path.read_text(encoding="utf-8").strip()
|
|
145
145
|
if not content:
|
|
146
146
|
return
|
|
147
147
|
data = json.loads(content)
|
package/core/workflow/loader.py
CHANGED
|
@@ -251,27 +251,6 @@
|
|
|
251
251
|
to { transform: translate3d(-2.5%, -1.5%, 0); }
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
/* Route transitions: one rhythm, upward continuity (enter rises, exit
|
|
255
|
-
recedes). Wired via app.pageTransition in nuxt.config. */
|
|
256
|
-
.arka-page-enter-active {
|
|
257
|
-
transition:
|
|
258
|
-
opacity var(--arka-motion-base) var(--arka-ease-out),
|
|
259
|
-
transform var(--arka-motion-base) var(--arka-ease-out);
|
|
260
|
-
}
|
|
261
|
-
.arka-page-leave-active {
|
|
262
|
-
transition:
|
|
263
|
-
opacity var(--arka-motion-fast) var(--arka-ease-in),
|
|
264
|
-
transform var(--arka-motion-fast) var(--arka-ease-in);
|
|
265
|
-
}
|
|
266
|
-
.arka-page-enter-from {
|
|
267
|
-
opacity: 0;
|
|
268
|
-
transform: translateY(8px);
|
|
269
|
-
}
|
|
270
|
-
.arka-page-leave-to {
|
|
271
|
-
opacity: 0;
|
|
272
|
-
transform: translateY(-4px);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
254
|
@media (prefers-reduced-motion: reduce) {
|
|
276
255
|
.arka-live-dot,
|
|
277
256
|
.arka-pulse-line,
|
|
@@ -279,12 +258,4 @@
|
|
|
279
258
|
.arka-starfield {
|
|
280
259
|
animation: none;
|
|
281
260
|
}
|
|
282
|
-
.arka-page-enter-active,
|
|
283
|
-
.arka-page-leave-active {
|
|
284
|
-
transition: none;
|
|
285
|
-
}
|
|
286
|
-
.arka-page-enter-from,
|
|
287
|
-
.arka-page-leave-to {
|
|
288
|
-
transform: none;
|
|
289
|
-
}
|
|
290
261
|
}
|
|
@@ -3,8 +3,39 @@ export const useApi = () => {
|
|
|
3
3
|
|
|
4
4
|
// path may be a plain string or a getter — useFetch accepts a getter URL
|
|
5
5
|
// and refetches when its reactive deps change (compare pages rely on it).
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
//
|
|
7
|
+
// An empty path means "nothing to fetch yet". Every empty-until-selected
|
|
8
|
+
// caller has that shape: the four compare pages return '' until their ids
|
|
9
|
+
// land (agents/compare.vue:49-54, departments/compare.vue:36-41,
|
|
10
|
+
// personas/compare.vue:75-80, personas/compare-with-agent.vue:54-59) and so
|
|
11
|
+
// does plan-canvas.vue:73-76 until a plan row is picked. useFetch has no
|
|
12
|
+
// skip flag, so an empty path collapses the URL to apiBase itself and 404s
|
|
13
|
+
// on mount. The guard therefore sits in the transport, never in
|
|
14
|
+
// `watch`/`immediate`: `watch: false` freezes the cache key at setup
|
|
15
|
+
// (fetch.js:106 passes `key.value`, not `key`), which merges two parallel
|
|
16
|
+
// fetches that start empty into one asyncData entry — both compare columns
|
|
17
|
+
// would then render the same record — and also kills the refetch of the
|
|
18
|
+
// pages that pass a reactive `query`.
|
|
19
|
+
//
|
|
20
|
+
// The app is client-only (`ssr: false`), so overriding `$fetch` costs
|
|
21
|
+
// nothing: useFetch's `useRequestFetch()` branch is server-side only, and
|
|
22
|
+
// apiBase is absolute, which disqualifies that branch anyway.
|
|
23
|
+
type Transport = typeof globalThis.$fetch
|
|
24
|
+
|
|
25
|
+
const fetchApi = <T>(path: MaybeRefOrGetter<string>, opts?: Record<string, unknown>) => {
|
|
26
|
+
const relative = computed(() => toValue(path))
|
|
27
|
+
const caller = opts?.$fetch as Transport | undefined
|
|
28
|
+
|
|
29
|
+
const skipWhenPathEmpty = ((...args: Parameters<Transport>) =>
|
|
30
|
+
relative.value
|
|
31
|
+
? (caller ?? globalThis.$fetch)(...args)
|
|
32
|
+
: Promise.resolve(undefined)) as Transport
|
|
33
|
+
|
|
34
|
+
return useFetch<T>(() => `${apiBase}${relative.value}`, {
|
|
35
|
+
...opts,
|
|
36
|
+
$fetch: skipWhenPathEmpty
|
|
37
|
+
})
|
|
38
|
+
}
|
|
8
39
|
|
|
9
40
|
return { fetchApi, apiBase }
|
|
10
41
|
}
|
package/dashboard/nuxt.config.ts
CHANGED
|
@@ -314,7 +314,7 @@ def generate_manifest(output_dir, video_info, frame_count, scroll_height,
|
|
|
314
314
|
manifest["mobile"] = _variant_summary(mobile_info)
|
|
315
315
|
|
|
316
316
|
manifest_path = Path(output_dir) / "manifest.json"
|
|
317
|
-
with open(manifest_path, "w") as f:
|
|
317
|
+
with open(manifest_path, "w", encoding="utf-8") as f:
|
|
318
318
|
json.dump(manifest, f, indent=2)
|
|
319
319
|
print(f"\n Manifest saved to: {manifest_path}", file=sys.stderr)
|
|
320
320
|
return manifest
|
|
@@ -109,7 +109,7 @@ def detect_stack(project_path: str) -> dict:
|
|
|
109
109
|
composer_path = p / "composer.json"
|
|
110
110
|
if composer_path.exists():
|
|
111
111
|
try:
|
|
112
|
-
composer = json.loads(composer_path.read_text())
|
|
112
|
+
composer = json.loads(composer_path.read_text(encoding="utf-8"))
|
|
113
113
|
require = {**composer.get("require", {}), **composer.get("require-dev", {})}
|
|
114
114
|
|
|
115
115
|
if "laravel/framework" in require:
|
|
@@ -162,7 +162,7 @@ def detect_stack(project_path: str) -> dict:
|
|
|
162
162
|
php_files = list(app_path.rglob("*.php"))[:5]
|
|
163
163
|
for pf in php_files:
|
|
164
164
|
try:
|
|
165
|
-
content = pf.read_text(errors="ignore")[:200]
|
|
165
|
+
content = pf.read_text(encoding="utf-8", errors="ignore")[:200]
|
|
166
166
|
if "declare(strict_types=1)" in content:
|
|
167
167
|
result["conventions"]["strict_types"] = True
|
|
168
168
|
break
|
|
@@ -176,7 +176,7 @@ def detect_stack(project_path: str) -> dict:
|
|
|
176
176
|
package_path = p / "package.json"
|
|
177
177
|
if package_path.exists():
|
|
178
178
|
try:
|
|
179
|
-
pkg = json.loads(package_path.read_text())
|
|
179
|
+
pkg = json.loads(package_path.read_text(encoding="utf-8"))
|
|
180
180
|
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
|
|
181
181
|
|
|
182
182
|
# Nuxt
|
|
@@ -261,7 +261,7 @@ def detect_stack(project_path: str) -> dict:
|
|
|
261
261
|
fp = p / f
|
|
262
262
|
if fp.exists():
|
|
263
263
|
try:
|
|
264
|
-
content = fp.read_text(errors="ignore")[:2000]
|
|
264
|
+
content = fp.read_text(encoding="utf-8", errors="ignore")[:2000]
|
|
265
265
|
if "fastapi" in content.lower() or "FastAPI" in content:
|
|
266
266
|
result["framework"] = "FastAPI"
|
|
267
267
|
result["stack"].append("FastAPI")
|
|
@@ -275,7 +275,7 @@ def detect_stack(project_path: str) -> dict:
|
|
|
275
275
|
env_file = p / ".env.example" if (p / ".env.example").exists() else p / ".env"
|
|
276
276
|
if env_file.exists():
|
|
277
277
|
try:
|
|
278
|
-
env_content = env_file.read_text(errors="ignore")
|
|
278
|
+
env_content = env_file.read_text(encoding="utf-8", errors="ignore")
|
|
279
279
|
if "DB_CONNECTION=pgsql" in env_content or "DATABASE_URL=postgres" in env_content:
|
|
280
280
|
result["database"].append("PostgreSQL")
|
|
281
281
|
elif "DB_CONNECTION=mysql" in env_content:
|
|
@@ -39,7 +39,7 @@ def _find_skills_dir() -> Path:
|
|
|
39
39
|
# Dev mode: .repo-path points to the repo checkout
|
|
40
40
|
repo_path_file = Path.home() / ".claude" / "skills" / "arka" / ".repo-path"
|
|
41
41
|
if repo_path_file.exists():
|
|
42
|
-
repo_path = Path(repo_path_file.read_text().strip())
|
|
42
|
+
repo_path = Path(repo_path_file.read_text(encoding="utf-8").strip())
|
|
43
43
|
if repo_path.is_dir():
|
|
44
44
|
return repo_path # Return repo root for dev mode resolution
|
|
45
45
|
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/scripts/dashboard-api.py
CHANGED
|
@@ -88,7 +88,7 @@ def _load_agents() -> list[dict]:
|
|
|
88
88
|
if _agents_cache is None:
|
|
89
89
|
path = ARKAOS_ROOT / "knowledge" / "agents-registry-v2.json"
|
|
90
90
|
if path.exists():
|
|
91
|
-
data = json.loads(path.read_text())
|
|
91
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
92
92
|
_agents_cache = data.get("agents", [])
|
|
93
93
|
else:
|
|
94
94
|
_agents_cache = []
|
|
@@ -100,7 +100,7 @@ def _load_commands() -> list[dict]:
|
|
|
100
100
|
if _commands_cache is None:
|
|
101
101
|
path = ARKAOS_ROOT / "knowledge" / "commands-registry.json"
|
|
102
102
|
if path.exists():
|
|
103
|
-
data = json.loads(path.read_text())
|
|
103
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
104
104
|
_commands_cache = data.get("commands", [])
|
|
105
105
|
else:
|
|
106
106
|
_commands_cache = []
|
|
@@ -739,7 +739,7 @@ def agent_detail(agent_id: str):
|
|
|
739
739
|
if yaml_file.exists():
|
|
740
740
|
try:
|
|
741
741
|
import yaml
|
|
742
|
-
raw = yaml.safe_load(yaml_file.read_text())
|
|
742
|
+
raw = yaml.safe_load(yaml_file.read_text(encoding="utf-8"))
|
|
743
743
|
dna = raw.get("behavioral_dna", {})
|
|
744
744
|
disc = dna.get("disc", {})
|
|
745
745
|
ennea = dna.get("enneagram", {})
|
|
@@ -4486,7 +4486,7 @@ def metrics():
|
|
|
4486
4486
|
if not metrics_file.exists():
|
|
4487
4487
|
return {"entries": [], "avg_ms": 0}
|
|
4488
4488
|
entries = []
|
|
4489
|
-
for line in metrics_file.read_text().strip().split("\n"):
|
|
4489
|
+
for line in metrics_file.read_text(encoding="utf-8").strip().split("\n"):
|
|
4490
4490
|
try:
|
|
4491
4491
|
entries.append(json.loads(line))
|
|
4492
4492
|
except Exception:
|
package/scripts/harness_gen.py
CHANGED
|
@@ -88,13 +88,13 @@ STACK_DISPLAY = {
|
|
|
88
88
|
|
|
89
89
|
def _load_agents() -> list[dict]:
|
|
90
90
|
data = json.loads(
|
|
91
|
-
(ROOT / "knowledge" / "agents-registry-v2.json").read_text())
|
|
91
|
+
(ROOT / "knowledge" / "agents-registry-v2.json").read_text(encoding="utf-8"))
|
|
92
92
|
return data["agents"]
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
def _load_command_counts() -> dict[str, int]:
|
|
96
96
|
data = json.loads(
|
|
97
|
-
(ROOT / "knowledge" / "commands-registry.json").read_text())
|
|
97
|
+
(ROOT / "knowledge" / "commands-registry.json").read_text(encoding="utf-8"))
|
|
98
98
|
return data["_meta"]["departments"]
|
|
99
99
|
|
|
100
100
|
|
|
@@ -114,7 +114,7 @@ def _split_frontmatter(text: str) -> tuple[list[str], str]:
|
|
|
114
114
|
def _stack_rules() -> list[tuple[str, list[str], str]]:
|
|
115
115
|
rules = []
|
|
116
116
|
for path in sorted(STACK_RULES_DIR.glob("*.md")):
|
|
117
|
-
globs, body = _split_frontmatter(path.read_text())
|
|
117
|
+
globs, body = _split_frontmatter(path.read_text(encoding="utf-8"))
|
|
118
118
|
rules.append((path.stem, globs, body))
|
|
119
119
|
return rules
|
|
120
120
|
|
|
@@ -402,7 +402,7 @@ def write_bundle(files: dict[str, str], harness_dir: Path) -> None:
|
|
|
402
402
|
for rel, content in files.items():
|
|
403
403
|
target = harness_dir / rel
|
|
404
404
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
405
|
-
target.write_text(content)
|
|
405
|
+
target.write_text(content, encoding="utf-8")
|
|
406
406
|
|
|
407
407
|
|
|
408
408
|
def main() -> int:
|
|
@@ -99,7 +99,7 @@ def load_agents_registry(root: Path) -> dict:
|
|
|
99
99
|
if not path.exists():
|
|
100
100
|
return {}
|
|
101
101
|
try:
|
|
102
|
-
data = json.loads(path.read_text())
|
|
102
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
103
103
|
agents = {}
|
|
104
104
|
for agent in data.get("agents", []):
|
|
105
105
|
agent_id = agent.get("id", "")
|
|
@@ -116,7 +116,7 @@ def load_commands_registry(root: Path) -> list:
|
|
|
116
116
|
if not path.exists():
|
|
117
117
|
return []
|
|
118
118
|
try:
|
|
119
|
-
data = json.loads(path.read_text())
|
|
119
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
120
120
|
return data.get("commands", [])
|
|
121
121
|
except Exception:
|
|
122
122
|
return []
|