arkaos 4.36.0 → 4.38.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/README.md +1 -1
- package/THE-ARKAOS-GUIDE.md +1 -1
- package/VERSION +1 -1
- package/arka/SKILL.md +1 -1
- package/config/skills-curated.yaml +2 -2
- package/config/skills-provenance.yaml +7 -0
- package/core/hooks/session_start.py +17 -3
- package/core/memory/semantic_store.py +58 -11
- package/core/memory/turn_capture.py +56 -11
- package/core/runtime/opencode.py +16 -5
- package/core/runtime/opencode_hooks.py +498 -0
- package/departments/dev/skills/animated-website/SKILL.md +4 -2
- package/departments/dev/skills/scroll-world/SKILL.md +619 -0
- package/departments/dev/skills/scroll-world/references/index-template.html +73 -0
- package/departments/dev/skills/scroll-world/references/knockout.py +89 -0
- package/departments/dev/skills/scroll-world/references/pipeline.md +213 -0
- package/departments/dev/skills/scroll-world/references/prompts.md +149 -0
- package/departments/dev/skills/scroll-world/references/scroll-world.LICENSE +21 -0
- package/departments/dev/skills/scroll-world/references/scrub-engine.js +448 -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 +128 -0
- package/harness/zed/.rules +1 -1
- package/installer/adapters/opencode.js +142 -16
- package/installer/assets/opencode/arka.ts +128 -0
- package/knowledge/skills-manifest.json +14 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/harness_gen.py +43 -3
|
@@ -0,0 +1,498 @@
|
|
|
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
|
+
+ detached turn capture into the shared cross-runtime
|
|
13
|
+
session memory (core.memory.turn_capture capture-text)
|
|
14
|
+
memory L9.5 parity — cross-runtime session memory retrieval +
|
|
15
|
+
once-per-session handoff digest on the prompt hot path
|
|
16
|
+
compact gate state context (PreCompact parity)
|
|
17
|
+
|
|
18
|
+
CLI contract::
|
|
19
|
+
|
|
20
|
+
echo '{"action": "pre_tool", ...}' | arka-py -m core.runtime.opencode_hooks
|
|
21
|
+
|
|
22
|
+
Output is a single JSON object on stdout. The module NEVER raises and
|
|
23
|
+
always exits 0 — a hook bridge must never break a turn (fail-open,
|
|
24
|
+
same posture as the bash hooks it mirrors).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import re
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
from datetime import datetime, timezone
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
_TELEMETRY_DIR = Path.home() / ".arkaos" / "telemetry"
|
|
38
|
+
_PROMPT_CACHE_DIR = Path.home() / ".arkaos" / "context-cache"
|
|
39
|
+
|
|
40
|
+
_BUILTIN_MAP = {
|
|
41
|
+
"webfetch": "WebFetch",
|
|
42
|
+
"websearch": "WebSearch",
|
|
43
|
+
"edit": "Edit",
|
|
44
|
+
"write": "Write",
|
|
45
|
+
"read": "Read",
|
|
46
|
+
"bash": "Bash",
|
|
47
|
+
"grep": "Grep",
|
|
48
|
+
"glob": "Glob",
|
|
49
|
+
"task": "Task",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_ARG_KEY_MAP = {
|
|
53
|
+
"filePath": "file_path",
|
|
54
|
+
"newString": "new_string",
|
|
55
|
+
"oldString": "old_string",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_VAGUE_RE = re.compile(
|
|
59
|
+
r"\b(fix the bug|that file|esse ficheiro|esse bug|o erro|aquilo)\b",
|
|
60
|
+
re.IGNORECASE,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
_STOPWORDS = frozenset({
|
|
64
|
+
"a", "o", "e", "de", "do", "da", "que", "em", "um", "uma", "para",
|
|
65
|
+
"com", "os", "as", "no", "na", "the", "and", "to", "of", "in", "is",
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _map_tool_name(name: str) -> str:
|
|
70
|
+
"""OpenCode ``server_tool`` / builtin -> Claude-style tool name."""
|
|
71
|
+
if name in _BUILTIN_MAP:
|
|
72
|
+
return _BUILTIN_MAP[name]
|
|
73
|
+
if name.startswith("mcp__"):
|
|
74
|
+
return name
|
|
75
|
+
server, sep, tool = name.partition("_")
|
|
76
|
+
if sep and server and tool:
|
|
77
|
+
return f"mcp__{server}__{tool}"
|
|
78
|
+
return name
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _map_args(args: dict) -> dict:
|
|
82
|
+
return {_ARG_KEY_MAP.get(k, k): v for k, v in (args or {}).items()}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _keywords(text: str) -> set[str]:
|
|
86
|
+
return {
|
|
87
|
+
w
|
|
88
|
+
for w in re.findall(r"[a-zA-ZÀ-ÿ]{4,}", text.lower())
|
|
89
|
+
if w not in _STOPWORDS
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _append_jsonl(path: Path, entry: dict) -> None:
|
|
94
|
+
try:
|
|
95
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
with path.open("a", encoding="utf-8") as fh:
|
|
97
|
+
fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
98
|
+
except OSError:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _recent_prompts(session_id: str) -> list[str]:
|
|
103
|
+
safe = re.sub(r"[^A-Za-z0-9_-]", "", session_id or "default")
|
|
104
|
+
path = _PROMPT_CACHE_DIR / f"opencode-prompts-{safe}.json"
|
|
105
|
+
try:
|
|
106
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
107
|
+
except (OSError, json.JSONDecodeError):
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _store_prompt(session_id: str, prompt: str) -> None:
|
|
112
|
+
safe = re.sub(r"[^A-Za-z0-9_-]", "", session_id or "default")
|
|
113
|
+
path = _PROMPT_CACHE_DIR / f"opencode-prompts-{safe}.json"
|
|
114
|
+
history = _recent_prompts(session_id)
|
|
115
|
+
history.append(prompt[:500])
|
|
116
|
+
_append = history[-3:]
|
|
117
|
+
try:
|
|
118
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
path.write_text(json.dumps(_append), encoding="utf-8")
|
|
120
|
+
except OSError:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _load_commands_registry() -> list[dict]:
|
|
125
|
+
"""knowledge/commands-registry.json from the repo root arka-py runs
|
|
126
|
+
against (PYTHONPATH chain: .repo-path -> dev checkout -> lib)."""
|
|
127
|
+
path = Path(__file__).resolve().parents[2] / "knowledge" / "commands-registry.json"
|
|
128
|
+
try:
|
|
129
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
130
|
+
commands = data.get("commands", [])
|
|
131
|
+
return commands if isinstance(commands, list) else []
|
|
132
|
+
except (OSError, json.JSONDecodeError):
|
|
133
|
+
return []
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _routing_lines(prompt: str, session_id: str, cwd: str) -> list[str]:
|
|
137
|
+
"""UserPromptSubmit routing parity (core/hooks/user_prompt_submit.py):
|
|
138
|
+
[ARKA:ROUTE] on every prompt + L1 [dept:X] + L5 [hint:/cmd] +
|
|
139
|
+
[ARKA:WORKFLOW-REQUIRED] on creation/implementation prompts. The
|
|
140
|
+
directive strings and the verb regex are IMPORTED from the claude
|
|
141
|
+
hook — one source of truth, zero drift."""
|
|
142
|
+
from core.hooks.user_prompt_submit import (
|
|
143
|
+
_ROUTE_REMINDER,
|
|
144
|
+
_WF_VERB_RE,
|
|
145
|
+
_WORKFLOW_DIRECTIVE,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
lines = [_ROUTE_REMINDER.strip()]
|
|
149
|
+
if not prompt:
|
|
150
|
+
return lines
|
|
151
|
+
try:
|
|
152
|
+
from core.synapse.layers import (
|
|
153
|
+
CommandHintsLayer,
|
|
154
|
+
DepartmentLayer,
|
|
155
|
+
PromptContext,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
ctx = PromptContext(
|
|
159
|
+
user_input=prompt,
|
|
160
|
+
cwd=cwd,
|
|
161
|
+
project_name=Path(cwd).name if cwd else "",
|
|
162
|
+
runtime_id="opencode",
|
|
163
|
+
extra={"session_id": session_id},
|
|
164
|
+
)
|
|
165
|
+
dept = DepartmentLayer().compute(ctx)
|
|
166
|
+
if dept.tag:
|
|
167
|
+
lines.append(dept.tag)
|
|
168
|
+
commands = _load_commands_registry()
|
|
169
|
+
if commands:
|
|
170
|
+
hints = CommandHintsLayer(commands=commands).compute(ctx)
|
|
171
|
+
if hints.tag:
|
|
172
|
+
lines.append(hints.tag)
|
|
173
|
+
except Exception: # noqa: BLE001 — routing hints are a bonus, never a blocker
|
|
174
|
+
pass
|
|
175
|
+
if prompt[0] not in ("/", "!") and _WF_VERB_RE.search(prompt):
|
|
176
|
+
lines.append(_WORKFLOW_DIRECTIVE.strip())
|
|
177
|
+
return lines
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _action_prompt(payload: dict) -> dict:
|
|
181
|
+
"""Token-hygiene parity (config/hooks/token-hygiene.sh) + the routing
|
|
182
|
+
injection (UserPromptSubmit parity) that makes /arka optional."""
|
|
183
|
+
prompt = str(payload.get("prompt", ""))
|
|
184
|
+
session_id = str(payload.get("session_id", ""))
|
|
185
|
+
suggestions: list[str] = []
|
|
186
|
+
routing = _routing_lines(prompt, session_id, str(payload.get("cwd", "")))
|
|
187
|
+
|
|
188
|
+
ctx = payload.get("context_pct")
|
|
189
|
+
if isinstance(ctx, (int, float)):
|
|
190
|
+
if ctx > 80:
|
|
191
|
+
suggestions.append(
|
|
192
|
+
f"[arka:warn] Contexto a {int(ctx)}% — considera /compact."
|
|
193
|
+
)
|
|
194
|
+
elif ctx > 60:
|
|
195
|
+
suggestions.append(
|
|
196
|
+
f"[arka:suggest] Contexto a {int(ctx)}% — vigia o orçamento."
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
history = _recent_prompts(session_id)
|
|
200
|
+
if history and prompt:
|
|
201
|
+
current = _keywords(prompt)
|
|
202
|
+
if current:
|
|
203
|
+
recent = set().union(*(_keywords(p) for p in history[-3:]))
|
|
204
|
+
if recent and len(current & recent) / len(current) < 0.3:
|
|
205
|
+
suggestions.append(
|
|
206
|
+
"[arka:suggest] Mudança de tópico — considera /clear "
|
|
207
|
+
"para libertar contexto."
|
|
208
|
+
)
|
|
209
|
+
if prompt:
|
|
210
|
+
_store_prompt(session_id, prompt)
|
|
211
|
+
|
|
212
|
+
if len(prompt) > 2000 and "```" in prompt:
|
|
213
|
+
suggestions.append(
|
|
214
|
+
"[arka:suggest] Paste grande — usa @filepath em vez de colar "
|
|
215
|
+
"código no prompt."
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if _VAGUE_RE.search(prompt) and "@" not in prompt:
|
|
219
|
+
suggestions.append(
|
|
220
|
+
"[arka:suggest] Referência vaga — usa @path para apontar o "
|
|
221
|
+
"ficheiro concreto."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return {"suggestions": suggestions, "routing": routing}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _action_pre_tool(payload: dict) -> dict:
|
|
228
|
+
tool = str(payload.get("tool", ""))
|
|
229
|
+
mapped = _map_tool_name(tool)
|
|
230
|
+
session_id = str(payload.get("session_id", ""))
|
|
231
|
+
args = _map_args(payload.get("args") or {})
|
|
232
|
+
|
|
233
|
+
from core.workflow import research_gate
|
|
234
|
+
|
|
235
|
+
decision = research_gate.evaluate_research_gate(
|
|
236
|
+
mapped,
|
|
237
|
+
session_id=session_id,
|
|
238
|
+
query=str(args.get("query", "") or args.get("url", "")),
|
|
239
|
+
)
|
|
240
|
+
if not decision.allow:
|
|
241
|
+
research_gate.record_telemetry(session_id, mapped, decision)
|
|
242
|
+
return {
|
|
243
|
+
"allow": False,
|
|
244
|
+
"reason": decision.reason,
|
|
245
|
+
"message": decision.to_stderr_message(),
|
|
246
|
+
}
|
|
247
|
+
if decision.nudge:
|
|
248
|
+
research_gate.record_telemetry(session_id, mapped, decision)
|
|
249
|
+
return {
|
|
250
|
+
"allow": True,
|
|
251
|
+
"reason": decision.reason,
|
|
252
|
+
"message": decision.to_stderr_message(),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if tool in ("edit", "write"):
|
|
256
|
+
from core.workflow import frontend_gate
|
|
257
|
+
|
|
258
|
+
fe = frontend_gate.evaluate(
|
|
259
|
+
_map_tool_name(tool),
|
|
260
|
+
"",
|
|
261
|
+
session_id,
|
|
262
|
+
str(payload.get("cwd", "")),
|
|
263
|
+
args,
|
|
264
|
+
messages=payload.get("messages") or [],
|
|
265
|
+
)
|
|
266
|
+
frontend_gate.record_telemetry(session_id, mapped, fe)
|
|
267
|
+
if not fe.allow or fe.reason == "no-design-marker":
|
|
268
|
+
return {
|
|
269
|
+
"allow": fe.allow,
|
|
270
|
+
"reason": fe.reason,
|
|
271
|
+
"message": fe.to_stderr_message(),
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {"allow": True, "reason": "ok", "message": ""}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _action_post_tool(payload: dict) -> dict:
|
|
278
|
+
tool = str(payload.get("tool", ""))
|
|
279
|
+
mapped = _map_tool_name(tool)
|
|
280
|
+
session_id = str(payload.get("session_id", ""))
|
|
281
|
+
|
|
282
|
+
from core.runtime import mcp_telemetry
|
|
283
|
+
|
|
284
|
+
recorded = mcp_telemetry.record(mapped, session_id=session_id)
|
|
285
|
+
_append_jsonl(
|
|
286
|
+
_TELEMETRY_DIR / "opencode-tools.jsonl",
|
|
287
|
+
{
|
|
288
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
289
|
+
"session": session_id,
|
|
290
|
+
"tool": mapped,
|
|
291
|
+
"ok": bool(payload.get("ok", True)),
|
|
292
|
+
},
|
|
293
|
+
)
|
|
294
|
+
return {"recorded_mcp": recorded}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _enqueue_turn_capture(session_id: str, text: str, cwd: str) -> None:
|
|
298
|
+
"""F1-A2 parity: fire-and-forget semantic turn capture — the same
|
|
299
|
+
detached-worker pattern as core/hooks/stop.py, fed with the assistant
|
|
300
|
+
text directly (OpenCode has no Claude-style JSONL transcript)."""
|
|
301
|
+
if not session_id or not text.strip():
|
|
302
|
+
return
|
|
303
|
+
repo = Path(__file__).resolve().parents[2]
|
|
304
|
+
if not (repo / "core" / "memory" / "turn_capture.py").is_file():
|
|
305
|
+
return
|
|
306
|
+
try:
|
|
307
|
+
proc = subprocess.Popen(
|
|
308
|
+
[sys.executable, "-m", "core.memory.turn_capture",
|
|
309
|
+
"capture-text", session_id, cwd or "", "opencode"],
|
|
310
|
+
stdin=subprocess.PIPE,
|
|
311
|
+
stdout=subprocess.DEVNULL,
|
|
312
|
+
stderr=subprocess.DEVNULL,
|
|
313
|
+
env={**os.environ, "PYTHONPATH": str(repo)},
|
|
314
|
+
start_new_session=True,
|
|
315
|
+
)
|
|
316
|
+
if proc.stdin:
|
|
317
|
+
try:
|
|
318
|
+
proc.stdin.write(text.encode("utf-8", "replace"))
|
|
319
|
+
proc.stdin.close()
|
|
320
|
+
except (BrokenPipeError, OSError):
|
|
321
|
+
pass
|
|
322
|
+
except Exception: # noqa: BLE001 — a hook bridge never breaks a turn
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _action_idle(payload: dict) -> dict:
|
|
327
|
+
"""Stop-hook parity: kb-citation + [arka:meta] compliance (soft),
|
|
328
|
+
plus detached turn capture into the shared cross-runtime memory."""
|
|
329
|
+
text = str(payload.get("response_text", ""))
|
|
330
|
+
session_id = str(payload.get("session_id", ""))
|
|
331
|
+
nudges: list[str] = []
|
|
332
|
+
|
|
333
|
+
from core.governance import kb_cite_check
|
|
334
|
+
|
|
335
|
+
result = kb_cite_check.check_citation(text)
|
|
336
|
+
if not result.passed and result.suggestion:
|
|
337
|
+
nudges.append(result.suggestion)
|
|
338
|
+
|
|
339
|
+
if len(text) > 400 and "[arka:meta]" not in text and "[arka:trivial]" not in text:
|
|
340
|
+
nudges.append(
|
|
341
|
+
"[arka:suggest] Resposta substantiva sem tag [arka:meta] — "
|
|
342
|
+
"fecha com kb=N research=X persona=Y gap=Z critic=W."
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
_enqueue_turn_capture(session_id, text, str(payload.get("cwd", "")))
|
|
346
|
+
|
|
347
|
+
_append_jsonl(
|
|
348
|
+
_TELEMETRY_DIR / "opencode-compliance.jsonl",
|
|
349
|
+
{
|
|
350
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
351
|
+
"session": session_id,
|
|
352
|
+
"kb_cite": result.reason,
|
|
353
|
+
"meta_tag": "[arka:meta]" in text,
|
|
354
|
+
"nudges": len(nudges),
|
|
355
|
+
},
|
|
356
|
+
)
|
|
357
|
+
return {"nudges": nudges}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
_HANDOFF_MAX_AGE_H = 24
|
|
361
|
+
_HANDOFF_SUMMARY_CHARS = 160
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _handoff_marker(session_id: str) -> Path:
|
|
365
|
+
safe = re.sub(r"[^A-Za-z0-9_-]", "", session_id or "default")
|
|
366
|
+
return _PROMPT_CACHE_DIR / f"opencode-handoff-{safe}.json"
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _handoff_line(session_id: str, project: str) -> str:
|
|
370
|
+
"""Once per session: when the newest turn in this project was captured
|
|
371
|
+
by ANOTHER runtime, surface it — that is the cross-runtime handoff."""
|
|
372
|
+
if not project or _handoff_marker(session_id).exists():
|
|
373
|
+
return ""
|
|
374
|
+
from core.memory.semantic_store import (
|
|
375
|
+
SessionMemoryStore,
|
|
376
|
+
default_db_path,
|
|
377
|
+
neutralize_summary,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
if not default_db_path().is_file():
|
|
381
|
+
return ""
|
|
382
|
+
latest = SessionMemoryStore().cross_runtime_handoff(
|
|
383
|
+
project, "opencode", exclude_session=session_id,
|
|
384
|
+
max_age_h=_HANDOFF_MAX_AGE_H,
|
|
385
|
+
)
|
|
386
|
+
if not latest:
|
|
387
|
+
return ""
|
|
388
|
+
try:
|
|
389
|
+
_handoff_marker(session_id).parent.mkdir(parents=True, exist_ok=True)
|
|
390
|
+
_handoff_marker(session_id).write_text(
|
|
391
|
+
json.dumps({"shown_at": datetime.now(timezone.utc).isoformat()}),
|
|
392
|
+
encoding="utf-8",
|
|
393
|
+
)
|
|
394
|
+
except OSError:
|
|
395
|
+
pass
|
|
396
|
+
summary = neutralize_summary(latest.summary)[:_HANDOFF_SUMMARY_CHARS]
|
|
397
|
+
stamp = latest.ts[11:16] + "Z" if len(latest.ts) >= 16 else latest.ts
|
|
398
|
+
tail = f": {summary}" if summary else ""
|
|
399
|
+
return f"[arka:handoff] última sessão em {latest.runtime} ({stamp}){tail}"
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _action_memory(payload: dict) -> dict:
|
|
403
|
+
"""L9.5 parity for the prompt hot path: pre-ranked semantic neighbours
|
|
404
|
+
+ live scoped keyword hits from the shared cross-runtime store, plus
|
|
405
|
+
the once-per-session handoff digest. Never embeds — zero cost here."""
|
|
406
|
+
prompt = str(payload.get("prompt", ""))
|
|
407
|
+
session_id = str(payload.get("session_id", ""))
|
|
408
|
+
cwd = str(payload.get("cwd", ""))
|
|
409
|
+
lines: list[str] = []
|
|
410
|
+
try:
|
|
411
|
+
from core.synapse.session_memory_layer import (
|
|
412
|
+
_format_item,
|
|
413
|
+
_l95_feature_flag_on,
|
|
414
|
+
_read_session_cache,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
if not _l95_feature_flag_on():
|
|
418
|
+
return {"context": []}
|
|
419
|
+
seen: set[str] = set()
|
|
420
|
+
items, ranked_at = _read_session_cache(session_id)
|
|
421
|
+
for item in items:
|
|
422
|
+
line = _format_item(item, ranked_at)
|
|
423
|
+
if line and item.get("summary") not in seen:
|
|
424
|
+
seen.add(item.get("summary"))
|
|
425
|
+
lines.append(line)
|
|
426
|
+
project = Path(cwd).name if cwd else ""
|
|
427
|
+
if prompt and project:
|
|
428
|
+
from core.memory.semantic_store import (
|
|
429
|
+
SessionMemoryStore,
|
|
430
|
+
default_db_path,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
if default_db_path().is_file():
|
|
434
|
+
hits = SessionMemoryStore().keyword_search(prompt, project, top_k=2)
|
|
435
|
+
for hit in hits:
|
|
436
|
+
line = _format_item(hit)
|
|
437
|
+
if line and hit.get("summary") not in seen:
|
|
438
|
+
seen.add(hit.get("summary"))
|
|
439
|
+
lines.append(line)
|
|
440
|
+
handoff = _handoff_line(session_id, project)
|
|
441
|
+
if handoff:
|
|
442
|
+
lines.insert(0, handoff)
|
|
443
|
+
except Exception: # noqa: BLE001 — memory is a bonus, never a blocker
|
|
444
|
+
return {"context": []}
|
|
445
|
+
return {"context": lines[:6]}
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _action_compact(payload: dict) -> dict:
|
|
449
|
+
"""PreCompact parity: carry gate state across compaction."""
|
|
450
|
+
from core.workflow import state
|
|
451
|
+
|
|
452
|
+
context: list[str] = [
|
|
453
|
+
"## ArkaOS gate state (injected by the opencode bridge)",
|
|
454
|
+
"Obrigatório: evidence flow G1 contexto -> G2 plano (aprovação) "
|
|
455
|
+
"-> G3 execução (teste real, exit 0) -> G4 review. Emite "
|
|
456
|
+
"[arka:gate:N] em cada gate; [arka:trivial] <razão> só para "
|
|
457
|
+
"edição de 1 ficheiro < 10 linhas.",
|
|
458
|
+
]
|
|
459
|
+
try:
|
|
460
|
+
current = state.get_state()
|
|
461
|
+
if current:
|
|
462
|
+
context.append(f"Workflow activo: {json.dumps(current)[:400]}")
|
|
463
|
+
except Exception: # noqa: BLE001 — fail-open
|
|
464
|
+
pass
|
|
465
|
+
return {"context": context}
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
_ACTIONS = {
|
|
469
|
+
"prompt": _action_prompt,
|
|
470
|
+
"pre_tool": _action_pre_tool,
|
|
471
|
+
"post_tool": _action_post_tool,
|
|
472
|
+
"idle": _action_idle,
|
|
473
|
+
"memory": _action_memory,
|
|
474
|
+
"compact": _action_compact,
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def main() -> int:
|
|
479
|
+
try:
|
|
480
|
+
payload = json.loads(sys.stdin.read() or "{}")
|
|
481
|
+
except json.JSONDecodeError:
|
|
482
|
+
payload = {}
|
|
483
|
+
action = str(payload.pop("action", ""))
|
|
484
|
+
handler = _ACTIONS.get(action)
|
|
485
|
+
result: dict
|
|
486
|
+
if handler is None:
|
|
487
|
+
result = {"error": f"unknown action: {action!r}"}
|
|
488
|
+
else:
|
|
489
|
+
try:
|
|
490
|
+
result = handler(payload)
|
|
491
|
+
except Exception as exc: # noqa: BLE001 — fail-open, never block
|
|
492
|
+
result = {"error": f"{type(exc).__name__}: {exc}"}
|
|
493
|
+
sys.stdout.write(json.dumps(result, ensure_ascii=False) + "\n")
|
|
494
|
+
return 0
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
if __name__ == "__main__":
|
|
498
|
+
raise SystemExit(main())
|
|
@@ -8,8 +8,10 @@ description: >
|
|
|
8
8
|
TRIGGER: "animated website", "scroll animation", "video to website",
|
|
9
9
|
"Apple-style page", "site com scroll do vídeo", "transforma este vídeo num
|
|
10
10
|
site", "frame animation", "/dev animated-website". SKIP: embedding a plain
|
|
11
|
-
video player -> just use <video>;
|
|
12
|
-
|
|
11
|
+
video player -> just use <video>; generating a scroll-scrubbed 3D world
|
|
12
|
+
FROM SCRATCH via Higgsfield (no existing video source) -> dev/scroll-world;
|
|
13
|
+
regular animated site WITHOUT a video source -> frontend-design skill;
|
|
14
|
+
video editing/production -> content/video-produce.
|
|
13
15
|
allowed-tools: [Read, Write, Edit, Bash, Grep, Glob, Agent, WebFetch]
|
|
14
16
|
metadata:
|
|
15
17
|
origin: community
|