arkaos 4.35.0 → 4.37.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/THE-ARKAOS-GUIDE.md +1 -1
- package/VERSION +1 -1
- package/bin/arka-menubar.py +58 -5
- package/core/runtime/opencode.py +168 -18
- package/core/runtime/opencode_hooks.py +313 -0
- package/harness/codex/AGENTS.md +1 -1
- package/harness/copilot/copilot-instructions.md +1 -1
- package/harness/cursor/rules/arkaos.mdc +2 -2
- package/harness/gemini/GEMINI.md +1 -1
- package/harness/opencode/AGENTS.md +1 -1
- package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
- package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
- package/harness/opencode/agents/arka-cfo-helena.md +1 -1
- package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
- package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
- package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
- package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
- package/harness/opencode/agents/arka-coo-sofia.md +1 -1
- package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
- package/harness/opencode/agents/arka-cqo-marta.md +1 -1
- package/harness/opencode/agents/arka-cto-marco.md +1 -1
- package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
- package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
- package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
- package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
- package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
- package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
- package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
- package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
- package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
- package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
- package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
- package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
- package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
- package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
- package/harness/opencode/agents-meta.json +27 -0
- package/harness/opencode/opencode.json +23 -0
- package/harness/opencode/plugins/arka.ts +113 -0
- package/harness/zed/.rules +1 -1
- package/installer/adapters/opencode.js +142 -16
- package/installer/assets/opencode/arka.ts +113 -0
- package/knowledge/skills-manifest.json +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/harness_gen.py +43 -3
package/THE-ARKAOS-GUIDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The ArkaOS Guide
|
|
2
2
|
|
|
3
|
-
> v4.
|
|
3
|
+
> v4.36.0 — 89 agents, 17 departments, 331 skills, 297 commands, 17 ADRs.
|
|
4
4
|
> One file, everything you need to start. Generated by `scripts/guide_gen.py` — never hand-edited.
|
|
5
5
|
|
|
6
6
|
## What it is
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.37.0
|
package/bin/arka-menubar.py
CHANGED
|
@@ -278,10 +278,44 @@ def run_app() -> int:
|
|
|
278
278
|
super().__init__(TITLE, quit_button=None)
|
|
279
279
|
self._rumps = rumps
|
|
280
280
|
self._pollers = set()
|
|
281
|
+
# QG r2 follow-ups: the ollama probe result is CACHED and
|
|
282
|
+
# refreshed by a worker (the 2s `ollama list` subprocess
|
|
283
|
+
# never runs on the menu runloop); the auto-update toggle
|
|
284
|
+
# is disabled while its worker is in flight.
|
|
285
|
+
self._ollama_cache = "absent"
|
|
286
|
+
self._probe_running = False
|
|
287
|
+
self._toggle_inflight = False
|
|
281
288
|
self.refresh(None)
|
|
282
289
|
self.timer = rumps.Timer(self.refresh, REFRESH_SECONDS)
|
|
283
290
|
self.timer.start()
|
|
284
291
|
|
|
292
|
+
def _schedule_ollama_probe(self):
|
|
293
|
+
"""Worker-side probe -> cache; redraw only on change."""
|
|
294
|
+
if self._probe_running:
|
|
295
|
+
return
|
|
296
|
+
self._probe_running = True
|
|
297
|
+
before = self._ollama_cache
|
|
298
|
+
|
|
299
|
+
def probe():
|
|
300
|
+
try:
|
|
301
|
+
self._ollama_cache = ollama_status()
|
|
302
|
+
finally:
|
|
303
|
+
self._probe_running = False
|
|
304
|
+
|
|
305
|
+
worker = threading.Thread(target=probe, daemon=True)
|
|
306
|
+
worker.start()
|
|
307
|
+
|
|
308
|
+
def poll(timer):
|
|
309
|
+
if not worker.is_alive():
|
|
310
|
+
timer.stop()
|
|
311
|
+
self._pollers.discard(timer)
|
|
312
|
+
if self._ollama_cache != before:
|
|
313
|
+
self.refresh(None)
|
|
314
|
+
|
|
315
|
+
poller = self._rumps.Timer(poll, 1)
|
|
316
|
+
self._pollers.add(poller)
|
|
317
|
+
poller.start()
|
|
318
|
+
|
|
285
319
|
def _spawn(self, work, refresh_after=False):
|
|
286
320
|
"""Run side-effect work off the AppKit main thread (a blocking
|
|
287
321
|
subprocess in a rumps callback freezes the whole menu bar).
|
|
@@ -313,7 +347,11 @@ def run_app() -> int:
|
|
|
313
347
|
|
|
314
348
|
def refresh(self, _sender):
|
|
315
349
|
state = read_state()
|
|
316
|
-
|
|
350
|
+
if state["profile"] == "local-ai":
|
|
351
|
+
ollama = self._ollama_cache
|
|
352
|
+
self._schedule_ollama_probe()
|
|
353
|
+
else:
|
|
354
|
+
ollama = "absent"
|
|
317
355
|
self.title = title_for(state)
|
|
318
356
|
self.menu.clear()
|
|
319
357
|
info = rumps.MenuItem(version_label(state))
|
|
@@ -331,8 +369,9 @@ def run_app() -> int:
|
|
|
331
369
|
"doctor": ("Run Doctor", self.on_doctor),
|
|
332
370
|
"start_ollama": ("Start Ollama", self.on_start_ollama),
|
|
333
371
|
"autoupdate_toggle": (
|
|
334
|
-
"Auto-update:
|
|
335
|
-
|
|
372
|
+
"Auto-update: switching…" if self._toggle_inflight
|
|
373
|
+
else ("Auto-update: on" if state["autoupdate_on"] else "Auto-update: off"),
|
|
374
|
+
None if self._toggle_inflight else self.on_autoupdate_toggle,
|
|
336
375
|
),
|
|
337
376
|
"disable": ("Disable menu bar (permanent)", self.on_disable),
|
|
338
377
|
"quit": ("Quit until next login", self.on_quit),
|
|
@@ -340,7 +379,7 @@ def run_app() -> int:
|
|
|
340
379
|
for item_id in visible:
|
|
341
380
|
label, callback = labels[item_id]
|
|
342
381
|
entry = rumps.MenuItem(label, callback=callback)
|
|
343
|
-
if item_id == "autoupdate_toggle":
|
|
382
|
+
if item_id == "autoupdate_toggle" and not self._toggle_inflight:
|
|
344
383
|
entry.state = 1 if state["autoupdate_on"] else 0
|
|
345
384
|
if item_id == "disable":
|
|
346
385
|
entries.append(rumps.separator)
|
|
@@ -365,8 +404,22 @@ def run_app() -> int:
|
|
|
365
404
|
self._spawn(action_start_ollama, refresh_after=True)
|
|
366
405
|
|
|
367
406
|
def on_autoupdate_toggle(self, _):
|
|
407
|
+
# Guard against double-fire while the (up to 180s) npx
|
|
408
|
+
# worker runs — the item is disabled and relabelled until
|
|
409
|
+
# the post-worker refresh (QG r2 minor).
|
|
410
|
+
if self._toggle_inflight:
|
|
411
|
+
return
|
|
412
|
+
self._toggle_inflight = True
|
|
368
413
|
enable = not read_state()["autoupdate_on"]
|
|
369
|
-
|
|
414
|
+
|
|
415
|
+
def work():
|
|
416
|
+
try:
|
|
417
|
+
action_autoupdate(enable=enable)
|
|
418
|
+
finally:
|
|
419
|
+
self._toggle_inflight = False
|
|
420
|
+
|
|
421
|
+
self._spawn(work, refresh_after=True)
|
|
422
|
+
self.refresh(None)
|
|
370
423
|
|
|
371
424
|
def on_disable(self, _):
|
|
372
425
|
try:
|
package/core/runtime/opencode.py
CHANGED
|
@@ -1,18 +1,40 @@
|
|
|
1
|
-
"""OpenCode runtime adapter (Foundation PR-6).
|
|
1
|
+
"""OpenCode runtime adapter (Foundation PR-6 + headless follow-up).
|
|
2
2
|
|
|
3
3
|
OpenCode (opencode.ai) — open-source terminal AI coding agent with
|
|
4
4
|
native agents (markdown, ``~/.config/opencode/agents/``), custom
|
|
5
|
-
commands (``commands/``), MCP servers (``opencode.json``),
|
|
6
|
-
instructions
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
commands (``commands/``), MCP servers (``opencode.json``), AGENTS.md
|
|
6
|
+
instructions, and a TypeScript plugin system (``plugins/``). The
|
|
7
|
+
installer adapter (installer/adapters/opencode.js) deploys the
|
|
8
|
+
generated harness bundle onto those surfaces, including
|
|
9
|
+
``plugins/arka.ts``, which bridges OpenCode events into
|
|
10
|
+
``core.runtime.opencode_hooks`` (kb-first research gate, frontend
|
|
11
|
+
gate, MCP telemetry, stop-hook compliance, compaction context).
|
|
12
|
+
|
|
13
|
+
Headless invocation (live-verified against the installed binary,
|
|
14
|
+
opencode 1.18.4, ``opencode run --help`` + a real probe, 2026-07-23):
|
|
15
|
+
|
|
16
|
+
opencode run --format json "<prompt>"
|
|
17
|
+
|
|
18
|
+
``--format json`` prints JSONL events to stdout. Live-verified shapes:
|
|
19
|
+
|
|
20
|
+
{"type":"step_start", "sessionID":"...", "part":{...}}
|
|
21
|
+
{"type":"text","part":{"type":"text","text":"ok",...}}
|
|
22
|
+
{"type":"step_finish","part":{"reason":"stop","tokens":
|
|
23
|
+
{"total":N,"input":N,"output":N,"reasoning":N,
|
|
24
|
+
"cache":{"write":N,"read":N}},"cost":X}}
|
|
25
|
+
{"type":"error","error":{"name":"...","data":{"message":"..."}}}
|
|
26
|
+
|
|
27
|
+
The error event fires with exit 1 when the user's DEFAULT model/provider
|
|
28
|
+
is broken (observed live) — headless_complete surfaces that as
|
|
29
|
+
LLMUnavailable with the event message. When no JSONL parses at all,
|
|
30
|
+
stdout is treated as raw text with a ``len(text) // 4`` token estimate
|
|
31
|
+
(same fallback as the Codex/Gemini adapters). stdin is closed explicitly
|
|
32
|
+
(codex precedent — a piped stdin must never leak into the prompt).
|
|
14
33
|
"""
|
|
15
34
|
|
|
35
|
+
import json
|
|
36
|
+
import shutil
|
|
37
|
+
import subprocess
|
|
16
38
|
from pathlib import Path
|
|
17
39
|
from os.path import expanduser
|
|
18
40
|
from typing import TYPE_CHECKING
|
|
@@ -23,6 +45,13 @@ if TYPE_CHECKING:
|
|
|
23
45
|
from core.runtime.llm_provider import LLMResponse
|
|
24
46
|
|
|
25
47
|
|
|
48
|
+
# `opencode run` is a full agent turn (session bootstrap + provider
|
|
49
|
+
# round-trip) — mirror the Codex budget, not the tighter Gemini one.
|
|
50
|
+
_TIMEOUT_SECONDS = 120
|
|
51
|
+
_TOKEN_ESTIMATE_DIVISOR = 4 # Rough chars-per-token heuristic.
|
|
52
|
+
_STDERR_CLIP = 200
|
|
53
|
+
|
|
54
|
+
|
|
26
55
|
class OpenCodeAdapter(RuntimeAdapter):
|
|
27
56
|
"""Adapter for OpenCode (terminal AI coding agent)."""
|
|
28
57
|
|
|
@@ -35,18 +64,25 @@ class OpenCodeAdapter(RuntimeAdapter):
|
|
|
35
64
|
config_dir=config_dir,
|
|
36
65
|
skills_dir=config_dir / "commands",
|
|
37
66
|
settings_file=config_dir / "opencode.json",
|
|
38
|
-
supports_hooks=
|
|
67
|
+
supports_hooks=(config_dir / "plugins" / "arka.ts").exists(),
|
|
39
68
|
supports_subagents=True,
|
|
40
69
|
supports_mcp=True,
|
|
41
70
|
max_context_tokens=200_000,
|
|
42
71
|
)
|
|
43
72
|
|
|
44
73
|
def capabilities(self) -> dict[str, bool]:
|
|
74
|
+
plugin = (
|
|
75
|
+
Path(expanduser("~")) / ".config" / "opencode" / "plugins" / "arka.ts"
|
|
76
|
+
)
|
|
45
77
|
return {
|
|
46
78
|
"agent_dispatch": False, # native agents exist; no ArkaOS wiring yet
|
|
47
|
-
"headless":
|
|
79
|
+
"headless": True, # `opencode run` live-verified 2026-07-23
|
|
48
80
|
"file_ops": True, # terminal agent edits files natively
|
|
49
|
-
|
|
81
|
+
# plugins/arka.ts (deployed by the installer adapter) bridges
|
|
82
|
+
# prompt/pre_tool/post_tool/idle/compact events into
|
|
83
|
+
# core.runtime.opencode_hooks — kb-first, frontend gate,
|
|
84
|
+
# telemetry, compliance. True only when actually deployed.
|
|
85
|
+
"hooks": plugin.exists(),
|
|
50
86
|
}
|
|
51
87
|
|
|
52
88
|
def inject_context(self, layers: dict[str, str]) -> str:
|
|
@@ -97,7 +133,7 @@ class OpenCodeAdapter(RuntimeAdapter):
|
|
|
97
133
|
raise NotImplementedError("Use OpenCode's native content search")
|
|
98
134
|
|
|
99
135
|
def headless_supported(self) -> bool:
|
|
100
|
-
return
|
|
136
|
+
return shutil.which("opencode") is not None
|
|
101
137
|
|
|
102
138
|
def headless_complete(
|
|
103
139
|
self,
|
|
@@ -106,8 +142,122 @@ class OpenCodeAdapter(RuntimeAdapter):
|
|
|
106
142
|
max_tokens: int = 2000,
|
|
107
143
|
system: str = "",
|
|
108
144
|
) -> "LLMResponse":
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
145
|
+
"""One-shot completion via `opencode run --format json`.
|
|
146
|
+
|
|
147
|
+
Verified against opencode 1.18.4 (live probe, 2026-07-23). The
|
|
148
|
+
model comes from the user's opencode config; a broken default
|
|
149
|
+
provider surfaces as an error event -> LLMUnavailable. Raises
|
|
150
|
+
LLMUnavailable on non-zero exit or timeout.
|
|
151
|
+
"""
|
|
152
|
+
from core.runtime.llm_provider import LLMUnavailable
|
|
153
|
+
|
|
154
|
+
binary = shutil.which("opencode")
|
|
155
|
+
if binary is None:
|
|
156
|
+
raise NotImplementedError(
|
|
157
|
+
"opencode CLI not found on PATH — install OpenCode to "
|
|
158
|
+
"enable headless completion."
|
|
159
|
+
)
|
|
160
|
+
effective_prompt = _merge_system_prompt(prompt, system)
|
|
161
|
+
cmd = [binary, "run", "--format", "json", effective_prompt]
|
|
162
|
+
proc = _run_opencode_cli(cmd)
|
|
163
|
+
if proc.returncode != 0:
|
|
164
|
+
# The error event carries the actionable message (observed
|
|
165
|
+
# live with a broken default model); stderr is often empty.
|
|
166
|
+
event_message = _first_error_message(proc.stdout)
|
|
167
|
+
detail = event_message or proc.stderr.strip()[:_STDERR_CLIP]
|
|
168
|
+
raise LLMUnavailable(
|
|
169
|
+
f"opencode run exited {proc.returncode}: {detail}"
|
|
170
|
+
)
|
|
171
|
+
return _parse_opencode_output(proc.stdout)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _merge_system_prompt(prompt: str, system: str) -> str:
|
|
175
|
+
# `opencode run` takes a single message; prepend the system text so
|
|
176
|
+
# downstream behaviour matches the other adapters.
|
|
177
|
+
if not system:
|
|
178
|
+
return prompt
|
|
179
|
+
return f"{system}\n\n---\n\n{prompt}"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _run_opencode_cli(cmd: list[str]) -> subprocess.CompletedProcess:
|
|
183
|
+
from core.runtime.llm_provider import LLMUnavailable
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
return subprocess.run(
|
|
187
|
+
cmd,
|
|
188
|
+
capture_output=True,
|
|
189
|
+
text=True,
|
|
190
|
+
encoding="utf-8",
|
|
191
|
+
errors="replace",
|
|
192
|
+
timeout=_TIMEOUT_SECONDS,
|
|
193
|
+
check=False,
|
|
194
|
+
# codex precedent: piped stdin must never leak into the run.
|
|
195
|
+
stdin=subprocess.DEVNULL,
|
|
196
|
+
)
|
|
197
|
+
except subprocess.TimeoutExpired as err:
|
|
198
|
+
raise LLMUnavailable(
|
|
199
|
+
f"opencode run timed out after {_TIMEOUT_SECONDS}s"
|
|
200
|
+
) from err
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _iter_events(stdout: str):
|
|
204
|
+
for line in stdout.splitlines():
|
|
205
|
+
line = line.strip()
|
|
206
|
+
if not line.startswith("{"):
|
|
207
|
+
continue
|
|
208
|
+
try:
|
|
209
|
+
yield json.loads(line)
|
|
210
|
+
except (ValueError, TypeError):
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _first_error_message(stdout: str) -> str:
|
|
215
|
+
for event in _iter_events(stdout):
|
|
216
|
+
if event.get("type") == "error":
|
|
217
|
+
data = (event.get("error") or {}).get("data") or {}
|
|
218
|
+
message = data.get("message") or (event.get("error") or {}).get("name")
|
|
219
|
+
if message:
|
|
220
|
+
return str(message)[:_STDERR_CLIP]
|
|
221
|
+
return ""
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _parse_opencode_output(stdout: str) -> "LLMResponse":
|
|
225
|
+
"""Parse the live-verified JSONL stream (see module docstring)."""
|
|
226
|
+
from core.runtime.llm_provider import LLMResponse, LLMUnavailable
|
|
227
|
+
|
|
228
|
+
texts: list[str] = []
|
|
229
|
+
tokens_in = 0
|
|
230
|
+
tokens_out = 0
|
|
231
|
+
cached = 0
|
|
232
|
+
saw_event = False
|
|
233
|
+
for event in _iter_events(stdout):
|
|
234
|
+
saw_event = True
|
|
235
|
+
kind = event.get("type")
|
|
236
|
+
part = event.get("part") or {}
|
|
237
|
+
if kind == "text" and part.get("type") == "text":
|
|
238
|
+
texts.append(str(part.get("text", "")))
|
|
239
|
+
elif kind == "step_finish":
|
|
240
|
+
tokens = part.get("tokens") or {}
|
|
241
|
+
tokens_in += int(tokens.get("input") or 0)
|
|
242
|
+
tokens_out += int(tokens.get("output") or 0)
|
|
243
|
+
cached += int((tokens.get("cache") or {}).get("read") or 0)
|
|
244
|
+
elif kind == "error":
|
|
245
|
+
message = _first_error_message(stdout) or "unknown opencode error"
|
|
246
|
+
raise LLMUnavailable(f"opencode run error event: {message}")
|
|
247
|
+
if saw_event:
|
|
248
|
+
return LLMResponse(
|
|
249
|
+
text="".join(texts),
|
|
250
|
+
tokens_in=tokens_in,
|
|
251
|
+
tokens_out=tokens_out,
|
|
252
|
+
cached_tokens=cached,
|
|
253
|
+
model="", # events carry no model id (verified 1.18.4)
|
|
113
254
|
)
|
|
255
|
+
# No JSONL parsed — degrade to raw text (codex/gemini fallback).
|
|
256
|
+
raw = stdout.strip()
|
|
257
|
+
return LLMResponse(
|
|
258
|
+
text=raw,
|
|
259
|
+
tokens_in=0,
|
|
260
|
+
tokens_out=max(1, len(raw) // _TOKEN_ESTIMATE_DIVISOR) if raw else 0,
|
|
261
|
+
cached_tokens=0,
|
|
262
|
+
model="",
|
|
263
|
+
)
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""OpenCode plugin bridge — JSON stdin/stdout hook adapter.
|
|
2
|
+
|
|
3
|
+
OpenCode has no shell-hook chain like Claude Code; its plugin system
|
|
4
|
+
(TypeScript, ``~/.config/opencode/plugins/``) emits events instead. The
|
|
5
|
+
``arka.ts`` plugin shells out to this module so the OpenCode runtime
|
|
6
|
+
gets the same governance stack as the Claude runtime:
|
|
7
|
+
|
|
8
|
+
prompt token-hygiene checks (UserPromptSubmit parity)
|
|
9
|
+
pre_tool research gate + frontend gate (PreToolUse parity)
|
|
10
|
+
post_tool MCP usage telemetry (PostToolUse parity)
|
|
11
|
+
idle kb-citation + [arka:meta] compliance (Stop-hook parity)
|
|
12
|
+
compact gate state context (PreCompact parity)
|
|
13
|
+
|
|
14
|
+
CLI contract::
|
|
15
|
+
|
|
16
|
+
echo '{"action": "pre_tool", ...}' | arka-py -m core.runtime.opencode_hooks
|
|
17
|
+
|
|
18
|
+
Output is a single JSON object on stdout. The module NEVER raises and
|
|
19
|
+
always exits 0 — a hook bridge must never break a turn (fail-open,
|
|
20
|
+
same posture as the bash hooks it mirrors).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import re
|
|
27
|
+
import sys
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
_TELEMETRY_DIR = Path.home() / ".arkaos" / "telemetry"
|
|
32
|
+
_PROMPT_CACHE_DIR = Path.home() / ".arkaos" / "context-cache"
|
|
33
|
+
|
|
34
|
+
_BUILTIN_MAP = {
|
|
35
|
+
"webfetch": "WebFetch",
|
|
36
|
+
"websearch": "WebSearch",
|
|
37
|
+
"edit": "Edit",
|
|
38
|
+
"write": "Write",
|
|
39
|
+
"read": "Read",
|
|
40
|
+
"bash": "Bash",
|
|
41
|
+
"grep": "Grep",
|
|
42
|
+
"glob": "Glob",
|
|
43
|
+
"task": "Task",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_ARG_KEY_MAP = {
|
|
47
|
+
"filePath": "file_path",
|
|
48
|
+
"newString": "new_string",
|
|
49
|
+
"oldString": "old_string",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_VAGUE_RE = re.compile(
|
|
53
|
+
r"\b(fix the bug|that file|esse ficheiro|esse bug|o erro|aquilo)\b",
|
|
54
|
+
re.IGNORECASE,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
_STOPWORDS = frozenset({
|
|
58
|
+
"a", "o", "e", "de", "do", "da", "que", "em", "um", "uma", "para",
|
|
59
|
+
"com", "os", "as", "no", "na", "the", "and", "to", "of", "in", "is",
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _map_tool_name(name: str) -> str:
|
|
64
|
+
"""OpenCode ``server_tool`` / builtin -> Claude-style tool name."""
|
|
65
|
+
if name in _BUILTIN_MAP:
|
|
66
|
+
return _BUILTIN_MAP[name]
|
|
67
|
+
if name.startswith("mcp__"):
|
|
68
|
+
return name
|
|
69
|
+
server, sep, tool = name.partition("_")
|
|
70
|
+
if sep and server and tool:
|
|
71
|
+
return f"mcp__{server}__{tool}"
|
|
72
|
+
return name
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _map_args(args: dict) -> dict:
|
|
76
|
+
return {_ARG_KEY_MAP.get(k, k): v for k, v in (args or {}).items()}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _keywords(text: str) -> set[str]:
|
|
80
|
+
return {
|
|
81
|
+
w
|
|
82
|
+
for w in re.findall(r"[a-zA-ZÀ-ÿ]{4,}", text.lower())
|
|
83
|
+
if w not in _STOPWORDS
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _append_jsonl(path: Path, entry: dict) -> None:
|
|
88
|
+
try:
|
|
89
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
with path.open("a", encoding="utf-8") as fh:
|
|
91
|
+
fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
92
|
+
except OSError:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _recent_prompts(session_id: str) -> list[str]:
|
|
97
|
+
safe = re.sub(r"[^A-Za-z0-9_-]", "", session_id or "default")
|
|
98
|
+
path = _PROMPT_CACHE_DIR / f"opencode-prompts-{safe}.json"
|
|
99
|
+
try:
|
|
100
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
101
|
+
except (OSError, json.JSONDecodeError):
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _store_prompt(session_id: str, prompt: str) -> None:
|
|
106
|
+
safe = re.sub(r"[^A-Za-z0-9_-]", "", session_id or "default")
|
|
107
|
+
path = _PROMPT_CACHE_DIR / f"opencode-prompts-{safe}.json"
|
|
108
|
+
history = _recent_prompts(session_id)
|
|
109
|
+
history.append(prompt[:500])
|
|
110
|
+
_append = history[-3:]
|
|
111
|
+
try:
|
|
112
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
path.write_text(json.dumps(_append), encoding="utf-8")
|
|
114
|
+
except OSError:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _action_prompt(payload: dict) -> dict:
|
|
119
|
+
"""Token-hygiene parity (config/hooks/token-hygiene.sh)."""
|
|
120
|
+
prompt = str(payload.get("prompt", ""))
|
|
121
|
+
session_id = str(payload.get("session_id", ""))
|
|
122
|
+
suggestions: list[str] = []
|
|
123
|
+
|
|
124
|
+
ctx = payload.get("context_pct")
|
|
125
|
+
if isinstance(ctx, (int, float)):
|
|
126
|
+
if ctx > 80:
|
|
127
|
+
suggestions.append(
|
|
128
|
+
f"[arka:warn] Contexto a {int(ctx)}% — considera /compact."
|
|
129
|
+
)
|
|
130
|
+
elif ctx > 60:
|
|
131
|
+
suggestions.append(
|
|
132
|
+
f"[arka:suggest] Contexto a {int(ctx)}% — vigia o orçamento."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
history = _recent_prompts(session_id)
|
|
136
|
+
if history and prompt:
|
|
137
|
+
current = _keywords(prompt)
|
|
138
|
+
if current:
|
|
139
|
+
recent = set().union(*(_keywords(p) for p in history[-3:]))
|
|
140
|
+
if recent and len(current & recent) / len(current) < 0.3:
|
|
141
|
+
suggestions.append(
|
|
142
|
+
"[arka:suggest] Mudança de tópico — considera /clear "
|
|
143
|
+
"para libertar contexto."
|
|
144
|
+
)
|
|
145
|
+
if prompt:
|
|
146
|
+
_store_prompt(session_id, prompt)
|
|
147
|
+
|
|
148
|
+
if len(prompt) > 2000 and "```" in prompt:
|
|
149
|
+
suggestions.append(
|
|
150
|
+
"[arka:suggest] Paste grande — usa @filepath em vez de colar "
|
|
151
|
+
"código no prompt."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if _VAGUE_RE.search(prompt) and "@" not in prompt:
|
|
155
|
+
suggestions.append(
|
|
156
|
+
"[arka:suggest] Referência vaga — usa @path para apontar o "
|
|
157
|
+
"ficheiro concreto."
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return {"suggestions": suggestions}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _action_pre_tool(payload: dict) -> dict:
|
|
164
|
+
tool = str(payload.get("tool", ""))
|
|
165
|
+
mapped = _map_tool_name(tool)
|
|
166
|
+
session_id = str(payload.get("session_id", ""))
|
|
167
|
+
args = _map_args(payload.get("args") or {})
|
|
168
|
+
|
|
169
|
+
from core.workflow import research_gate
|
|
170
|
+
|
|
171
|
+
decision = research_gate.evaluate_research_gate(
|
|
172
|
+
mapped,
|
|
173
|
+
session_id=session_id,
|
|
174
|
+
query=str(args.get("query", "") or args.get("url", "")),
|
|
175
|
+
)
|
|
176
|
+
if not decision.allow:
|
|
177
|
+
research_gate.record_telemetry(session_id, mapped, decision)
|
|
178
|
+
return {
|
|
179
|
+
"allow": False,
|
|
180
|
+
"reason": decision.reason,
|
|
181
|
+
"message": decision.to_stderr_message(),
|
|
182
|
+
}
|
|
183
|
+
if decision.nudge:
|
|
184
|
+
research_gate.record_telemetry(session_id, mapped, decision)
|
|
185
|
+
return {
|
|
186
|
+
"allow": True,
|
|
187
|
+
"reason": decision.reason,
|
|
188
|
+
"message": decision.to_stderr_message(),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if tool in ("edit", "write"):
|
|
192
|
+
from core.workflow import frontend_gate
|
|
193
|
+
|
|
194
|
+
fe = frontend_gate.evaluate(
|
|
195
|
+
_map_tool_name(tool),
|
|
196
|
+
"",
|
|
197
|
+
session_id,
|
|
198
|
+
str(payload.get("cwd", "")),
|
|
199
|
+
args,
|
|
200
|
+
messages=payload.get("messages") or [],
|
|
201
|
+
)
|
|
202
|
+
frontend_gate.record_telemetry(session_id, mapped, fe)
|
|
203
|
+
if not fe.allow or fe.reason == "no-design-marker":
|
|
204
|
+
return {
|
|
205
|
+
"allow": fe.allow,
|
|
206
|
+
"reason": fe.reason,
|
|
207
|
+
"message": fe.to_stderr_message(),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {"allow": True, "reason": "ok", "message": ""}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _action_post_tool(payload: dict) -> dict:
|
|
214
|
+
tool = str(payload.get("tool", ""))
|
|
215
|
+
mapped = _map_tool_name(tool)
|
|
216
|
+
session_id = str(payload.get("session_id", ""))
|
|
217
|
+
|
|
218
|
+
from core.runtime import mcp_telemetry
|
|
219
|
+
|
|
220
|
+
recorded = mcp_telemetry.record(mapped, session_id=session_id)
|
|
221
|
+
_append_jsonl(
|
|
222
|
+
_TELEMETRY_DIR / "opencode-tools.jsonl",
|
|
223
|
+
{
|
|
224
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
225
|
+
"session": session_id,
|
|
226
|
+
"tool": mapped,
|
|
227
|
+
"ok": bool(payload.get("ok", True)),
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
return {"recorded_mcp": recorded}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _action_idle(payload: dict) -> dict:
|
|
234
|
+
"""Stop-hook parity: kb-citation + [arka:meta] compliance (soft)."""
|
|
235
|
+
text = str(payload.get("response_text", ""))
|
|
236
|
+
session_id = str(payload.get("session_id", ""))
|
|
237
|
+
nudges: list[str] = []
|
|
238
|
+
|
|
239
|
+
from core.governance import kb_cite_check
|
|
240
|
+
|
|
241
|
+
result = kb_cite_check.check_citation(text)
|
|
242
|
+
if not result.passed and result.suggestion:
|
|
243
|
+
nudges.append(result.suggestion)
|
|
244
|
+
|
|
245
|
+
if len(text) > 400 and "[arka:meta]" not in text and "[arka:trivial]" not in text:
|
|
246
|
+
nudges.append(
|
|
247
|
+
"[arka:suggest] Resposta substantiva sem tag [arka:meta] — "
|
|
248
|
+
"fecha com kb=N research=X persona=Y gap=Z critic=W."
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
_append_jsonl(
|
|
252
|
+
_TELEMETRY_DIR / "opencode-compliance.jsonl",
|
|
253
|
+
{
|
|
254
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
255
|
+
"session": session_id,
|
|
256
|
+
"kb_cite": result.reason,
|
|
257
|
+
"meta_tag": "[arka:meta]" in text,
|
|
258
|
+
"nudges": len(nudges),
|
|
259
|
+
},
|
|
260
|
+
)
|
|
261
|
+
return {"nudges": nudges}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _action_compact(payload: dict) -> dict:
|
|
265
|
+
"""PreCompact parity: carry gate state across compaction."""
|
|
266
|
+
from core.workflow import state
|
|
267
|
+
|
|
268
|
+
context: list[str] = [
|
|
269
|
+
"## ArkaOS gate state (injected by the opencode bridge)",
|
|
270
|
+
"Obrigatório: evidence flow G1 contexto -> G2 plano (aprovação) "
|
|
271
|
+
"-> G3 execução (teste real, exit 0) -> G4 review. Emite "
|
|
272
|
+
"[arka:gate:N] em cada gate; [arka:trivial] <razão> só para "
|
|
273
|
+
"edição de 1 ficheiro < 10 linhas.",
|
|
274
|
+
]
|
|
275
|
+
try:
|
|
276
|
+
current = state.get_state()
|
|
277
|
+
if current:
|
|
278
|
+
context.append(f"Workflow activo: {json.dumps(current)[:400]}")
|
|
279
|
+
except Exception: # noqa: BLE001 — fail-open
|
|
280
|
+
pass
|
|
281
|
+
return {"context": context}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
_ACTIONS = {
|
|
285
|
+
"prompt": _action_prompt,
|
|
286
|
+
"pre_tool": _action_pre_tool,
|
|
287
|
+
"post_tool": _action_post_tool,
|
|
288
|
+
"idle": _action_idle,
|
|
289
|
+
"compact": _action_compact,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def main() -> int:
|
|
294
|
+
try:
|
|
295
|
+
payload = json.loads(sys.stdin.read() or "{}")
|
|
296
|
+
except json.JSONDecodeError:
|
|
297
|
+
payload = {}
|
|
298
|
+
action = str(payload.pop("action", ""))
|
|
299
|
+
handler = _ACTIONS.get(action)
|
|
300
|
+
result: dict
|
|
301
|
+
if handler is None:
|
|
302
|
+
result = {"error": f"unknown action: {action!r}"}
|
|
303
|
+
else:
|
|
304
|
+
try:
|
|
305
|
+
result = handler(payload)
|
|
306
|
+
except Exception as exc: # noqa: BLE001 — fail-open, never block
|
|
307
|
+
result = {"error": f"{type(exc).__name__}: {exc}"}
|
|
308
|
+
sys.stdout.write(json.dumps(result, ensure_ascii=False) + "\n")
|
|
309
|
+
return 0
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
if __name__ == "__main__":
|
|
313
|
+
raise SystemExit(main())
|
package/harness/codex/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ArkaOS — The Operating System for AI Agent Teams
|
|
2
2
|
|
|
3
|
-
> v4.
|
|
3
|
+
> v4.37.0 — 89 agents, 17 departments, 331 skills. Generated by `scripts/harness_gen.py`; do not edit.
|
|
4
4
|
|
|
5
5
|
You are operating within ArkaOS. Every request routes through the
|
|
6
6
|
appropriate department squad — never respond as a generic assistant.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ArkaOS — The Operating System for AI Agent Teams
|
|
2
2
|
|
|
3
|
-
> v4.
|
|
3
|
+
> v4.37.0 — 89 agents, 17 departments, 331 skills. Generated by `scripts/harness_gen.py`; do not edit.
|
|
4
4
|
|
|
5
5
|
You are operating within ArkaOS. Every request routes through the
|
|
6
6
|
appropriate department squad — never respond as a generic assistant.
|