arkaos 4.37.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_hooks.py +188 -3
- 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/plugins/arka.ts +15 -0
- package/harness/zed/.rules +1 -1
- package/installer/assets/opencode/arka.ts +15 -0
- package/knowledge/skills-manifest.json +14 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/README.md
CHANGED
package/THE-ARKAOS-GUIDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The ArkaOS Guide
|
|
2
2
|
|
|
3
|
-
> v4.
|
|
3
|
+
> v4.38.0 — 89 agents, 17 departments, 332 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.38.0
|
package/arka/SKILL.md
CHANGED
|
@@ -47,7 +47,7 @@ complementary, not alternatives.
|
|
|
47
47
|
# ArkaOS — Main Orchestrator
|
|
48
48
|
|
|
49
49
|
> **The Operating System for AI Agent Teams**
|
|
50
|
-
> 89 agents. 17 departments.
|
|
50
|
+
> 89 agents. 17 departments. 332 skills. Multi-runtime. Dashboard. Knowledge RAG.
|
|
51
51
|
|
|
52
52
|
## ⛔ Evidence flow — 4 gates (NON-NEGOTIABLE)
|
|
53
53
|
|
|
@@ -24,8 +24,8 @@ curated_subskills:
|
|
|
24
24
|
community: [moderation]
|
|
25
25
|
content: [calendar, hook-write]
|
|
26
26
|
dev: [api-design, architecture-design, clean-code-review, code-review,
|
|
27
|
-
db-design, diagram, refactor-plan, runbook,
|
|
28
|
-
tdd-cycle, watch]
|
|
27
|
+
db-design, diagram, refactor-plan, runbook, scroll-world,
|
|
28
|
+
security-audit, spec, tdd-cycle, watch]
|
|
29
29
|
ecom: [cart-recovery, store-audit]
|
|
30
30
|
finance: [financial-model, unit-economics]
|
|
31
31
|
kb: [search-kb, zettelkasten-process]
|
|
@@ -91,6 +91,13 @@ derived:
|
|
|
91
91
|
origin: community
|
|
92
92
|
source: https://github.com/nutlope/hallmark
|
|
93
93
|
license: MIT
|
|
94
|
+
# Scroll-world — scroll-scrubbed "fly through the world" landing pages
|
|
95
|
+
# (Higgsfield pipeline + portable scrub engine), vendored verbatim with
|
|
96
|
+
# ArkaOS frontmatter; THIRD-PARTY-NOTICES records the trail.
|
|
97
|
+
departments/dev/skills/scroll-world:
|
|
98
|
+
origin: community
|
|
99
|
+
source: https://github.com/oso95/scroll-world
|
|
100
|
+
license: MIT
|
|
94
101
|
# Watch absorption campaign — video-input primitive derived from
|
|
95
102
|
# bradautomates/claude-video v0.2.0 (MIT), maintained natively.
|
|
96
103
|
departments/dev/skills/watch:
|
|
@@ -341,16 +341,30 @@ def build_recap(cwd: str, budget_ms: int = _BUDGET_MS) -> str:
|
|
|
341
341
|
if not records or (time.monotonic() - start) * 1000 > budget_ms:
|
|
342
342
|
return ""
|
|
343
343
|
lines = ["[SESSION-MEMORY] Prior turns (importance+recency — not semantic):"]
|
|
344
|
+
handoff = store.cross_runtime_handoff(project, "claude")
|
|
345
|
+
if handoff:
|
|
346
|
+
stamp = (
|
|
347
|
+
handoff.ts[11:16] + "Z" if len(handoff.ts) >= 16 else handoff.ts
|
|
348
|
+
)
|
|
349
|
+
h_summary = neutralize_summary(handoff.summary)[:_SUMMARY_CHARS]
|
|
350
|
+
tail = f": {h_summary}" if h_summary else ""
|
|
351
|
+
lines.append(
|
|
352
|
+
f"[arka:handoff] última sessão em {handoff.runtime}"
|
|
353
|
+
f" ({stamp}){tail}"
|
|
354
|
+
)
|
|
355
|
+
turn_lines = 0
|
|
344
356
|
for record in records:
|
|
345
357
|
summary = neutralize_summary(record.summary)[:_SUMMARY_CHARS]
|
|
346
358
|
if not summary:
|
|
347
359
|
continue
|
|
348
|
-
|
|
349
|
-
|
|
360
|
+
runtime = f" · {record.runtime}" if record.runtime else ""
|
|
361
|
+
lines.append(f"[SESSION-MEMORY] - {record.ts[:10]}{runtime}: {summary}")
|
|
362
|
+
turn_lines += 1
|
|
363
|
+
if turn_lines == 0:
|
|
350
364
|
return ""
|
|
351
365
|
backends = ",".join(sorted({r.embedding_backend for r in records})) or "none"
|
|
352
366
|
lines.append(
|
|
353
|
-
f"[SESSION-MEMORY] shown: {
|
|
367
|
+
f"[SESSION-MEMORY] shown: {turn_lines} turns ({project}),"
|
|
354
368
|
f" backends={backends}"
|
|
355
369
|
)
|
|
356
370
|
return "\n".join(lines)
|
|
@@ -46,6 +46,7 @@ class TurnRecord(BaseModel):
|
|
|
46
46
|
id: str = Field(default_factory=lambda: uuid.uuid4().hex)
|
|
47
47
|
ts: str = ""
|
|
48
48
|
session_id: str = ""
|
|
49
|
+
runtime: str = ""
|
|
49
50
|
project_name: str = ""
|
|
50
51
|
cwd: str = ""
|
|
51
52
|
summary: str = ""
|
|
@@ -110,6 +111,7 @@ class SessionMemoryStore:
|
|
|
110
111
|
id TEXT PRIMARY KEY,
|
|
111
112
|
ts TEXT NOT NULL,
|
|
112
113
|
session_id TEXT NOT NULL,
|
|
114
|
+
runtime TEXT NOT NULL DEFAULT '',
|
|
113
115
|
project_name TEXT NOT NULL DEFAULT '',
|
|
114
116
|
cwd TEXT NOT NULL DEFAULT '',
|
|
115
117
|
summary TEXT NOT NULL DEFAULT '',
|
|
@@ -122,13 +124,23 @@ class SessionMemoryStore:
|
|
|
122
124
|
dims INTEGER NOT NULL DEFAULT 0
|
|
123
125
|
)
|
|
124
126
|
""")
|
|
127
|
+
self._migrate(conn)
|
|
128
|
+
|
|
129
|
+
def _migrate(self, conn: sqlite3.Connection) -> None:
|
|
130
|
+
"""Additive column migrations for DBs created before a column
|
|
131
|
+
existed — CREATE TABLE IF NOT EXISTS never alters a live table."""
|
|
132
|
+
cols = {row[1] for row in conn.execute("PRAGMA table_info(turns)")}
|
|
133
|
+
if "runtime" not in cols:
|
|
125
134
|
conn.execute(
|
|
126
|
-
"
|
|
127
|
-
"ON turns (project_name, ts DESC)"
|
|
128
|
-
)
|
|
129
|
-
conn.execute(
|
|
130
|
-
"CREATE INDEX IF NOT EXISTS idx_turns_session ON turns (session_id)"
|
|
135
|
+
"ALTER TABLE turns ADD COLUMN runtime TEXT NOT NULL DEFAULT ''"
|
|
131
136
|
)
|
|
137
|
+
conn.execute(
|
|
138
|
+
"CREATE INDEX IF NOT EXISTS idx_turns_project_ts "
|
|
139
|
+
"ON turns (project_name, ts DESC)"
|
|
140
|
+
)
|
|
141
|
+
conn.execute(
|
|
142
|
+
"CREATE INDEX IF NOT EXISTS idx_turns_session ON turns (session_id)"
|
|
143
|
+
)
|
|
132
144
|
|
|
133
145
|
def save(self, record: TurnRecord) -> None:
|
|
134
146
|
row = record.model_dump()
|
|
@@ -139,12 +151,13 @@ class SessionMemoryStore:
|
|
|
139
151
|
)
|
|
140
152
|
with closing(self._conn()) as conn, conn:
|
|
141
153
|
conn.execute(
|
|
142
|
-
"INSERT OR REPLACE INTO turns (id, ts, session_id,
|
|
143
|
-
" cwd, summary, tools_used, file_paths,
|
|
144
|
-
" embedding_backend, embedding_model,
|
|
145
|
-
"
|
|
146
|
-
" :
|
|
147
|
-
" :
|
|
154
|
+
"INSERT OR REPLACE INTO turns (id, ts, session_id, runtime,"
|
|
155
|
+
" project_name, cwd, summary, tools_used, file_paths,"
|
|
156
|
+
" importance, embedding, embedding_backend, embedding_model,"
|
|
157
|
+
" dims)"
|
|
158
|
+
" VALUES (:id, :ts, :session_id, :runtime, :project_name,"
|
|
159
|
+
" :cwd, :summary, :tools_used, :file_paths, :importance,"
|
|
160
|
+
" :embedding, :embedding_backend, :embedding_model, :dims)",
|
|
148
161
|
row,
|
|
149
162
|
)
|
|
150
163
|
|
|
@@ -185,6 +198,40 @@ class SessionMemoryStore:
|
|
|
185
198
|
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
|
|
186
199
|
return where, params
|
|
187
200
|
|
|
201
|
+
def latest_turn(
|
|
202
|
+
self, project_name: str, exclude_session: str | None = None
|
|
203
|
+
) -> TurnRecord | None:
|
|
204
|
+
"""Newest turn in the project — the handoff-detection primitive."""
|
|
205
|
+
where, params = self._scope(project_name, exclude_session)
|
|
206
|
+
with closing(self._conn()) as conn:
|
|
207
|
+
row = conn.execute(
|
|
208
|
+
f"SELECT * FROM turns {where} ORDER BY ts DESC LIMIT 1",
|
|
209
|
+
params,
|
|
210
|
+
).fetchone()
|
|
211
|
+
return self._row_to_record(row) if row else None
|
|
212
|
+
|
|
213
|
+
def cross_runtime_handoff(
|
|
214
|
+
self,
|
|
215
|
+
project_name: str,
|
|
216
|
+
current_runtime: str,
|
|
217
|
+
exclude_session: str | None = None,
|
|
218
|
+
max_age_h: float = 24,
|
|
219
|
+
) -> TurnRecord | None:
|
|
220
|
+
"""Newest turn in the project captured by a DIFFERENT runtime,
|
|
221
|
+
fresh enough to matter as a continuation hint — the shared
|
|
222
|
+
handoff predicate for every runtime's entry point."""
|
|
223
|
+
latest = self.latest_turn(project_name, exclude_session=exclude_session)
|
|
224
|
+
if not latest or not latest.runtime or latest.runtime == current_runtime:
|
|
225
|
+
return None
|
|
226
|
+
try:
|
|
227
|
+
age_h = (
|
|
228
|
+
datetime.now(UTC)
|
|
229
|
+
- datetime.fromisoformat(latest.ts.replace("Z", "+00:00"))
|
|
230
|
+
).total_seconds() / 3600
|
|
231
|
+
except ValueError:
|
|
232
|
+
return None
|
|
233
|
+
return latest if age_h <= max_age_h else None
|
|
234
|
+
|
|
188
235
|
def keyword_search(
|
|
189
236
|
self, query: str, project_name: str | None = None, top_k: int = 3
|
|
190
237
|
) -> list[dict]:
|
|
@@ -161,7 +161,8 @@ def _precompute_cache(
|
|
|
161
161
|
"dims": record.dims,
|
|
162
162
|
"items": [
|
|
163
163
|
{"summary": i["summary"][:200], "project_name": i["project_name"],
|
|
164
|
-
"ts": i["ts"], "score": i["score"], "retrieval": i["retrieval"]
|
|
164
|
+
"ts": i["ts"], "score": i["score"], "retrieval": i["retrieval"],
|
|
165
|
+
"runtime": i.get("runtime", "")}
|
|
165
166
|
for i in items
|
|
166
167
|
],
|
|
167
168
|
}
|
|
@@ -190,14 +191,16 @@ def _maintenance(store: SessionMemoryStore) -> None:
|
|
|
190
191
|
store.prune()
|
|
191
192
|
|
|
192
193
|
|
|
193
|
-
def
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
194
|
+
def _persist_record(
|
|
195
|
+
session_id: str,
|
|
196
|
+
text: str,
|
|
197
|
+
cwd: str,
|
|
198
|
+
runtime: str,
|
|
199
|
+
tools: list[str],
|
|
200
|
+
paths: list[str],
|
|
201
|
+
) -> None:
|
|
202
|
+
"""Shared tail for every capture path: sanitize -> embed -> store ->
|
|
203
|
+
precompute the F1-A3 neighbour cache -> amortised maintenance."""
|
|
201
204
|
summary, sanitized_ok = _sanitized_summary(text)
|
|
202
205
|
tools = _safe_tool_names(tools, sanitized_ok)
|
|
203
206
|
if not sanitized_ok:
|
|
@@ -206,6 +209,7 @@ def capture_turn(session_id: str, transcript_path: str, cwd: str = "") -> int:
|
|
|
206
209
|
record = TurnRecord(
|
|
207
210
|
ts=datetime.now(UTC).isoformat(),
|
|
208
211
|
session_id=session_id,
|
|
212
|
+
runtime=runtime,
|
|
209
213
|
project_name=Path(cwd).name if cwd else "",
|
|
210
214
|
cwd=cwd,
|
|
211
215
|
summary=summary,
|
|
@@ -221,6 +225,31 @@ def capture_turn(session_id: str, transcript_path: str, cwd: str = "") -> int:
|
|
|
221
225
|
store.save(record)
|
|
222
226
|
_precompute_cache(store, record, session_id)
|
|
223
227
|
_maintenance(store)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def capture_turn(
|
|
231
|
+
session_id: str, transcript_path: str, cwd: str = "", runtime: str = "claude"
|
|
232
|
+
) -> int:
|
|
233
|
+
if not _config_enabled() or not session_id:
|
|
234
|
+
return 0
|
|
235
|
+
raw = _read_tail(transcript_path)
|
|
236
|
+
if not raw:
|
|
237
|
+
return 0
|
|
238
|
+
text = _last_assistant_text(transcript_path, raw)
|
|
239
|
+
tools, paths = _parse_tool_uses(raw)
|
|
240
|
+
_persist_record(session_id, text, cwd, runtime, tools, paths)
|
|
241
|
+
return 0
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def capture_text_turn(
|
|
245
|
+
session_id: str, text: str, cwd: str = "", runtime: str = "opencode"
|
|
246
|
+
) -> int:
|
|
247
|
+
"""Transcript-free capture for runtimes without a Claude-style JSONL
|
|
248
|
+
transcript on disk (OpenCode: the plugin holds the messages, not us).
|
|
249
|
+
The caller hands over the last assistant text directly."""
|
|
250
|
+
if not _config_enabled() or not session_id or not text.strip():
|
|
251
|
+
return 0
|
|
252
|
+
_persist_record(session_id, text, cwd, runtime, [], [])
|
|
224
253
|
return 0
|
|
225
254
|
|
|
226
255
|
|
|
@@ -261,14 +290,30 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
261
290
|
return run_maintenance()
|
|
262
291
|
except Exception: # detached worker must die quietly
|
|
263
292
|
return 0
|
|
293
|
+
if args and args[0] == "capture-text":
|
|
294
|
+
if len(args) < 4:
|
|
295
|
+
print(
|
|
296
|
+
"usage: python3 -m core.memory.turn_capture"
|
|
297
|
+
" capture-text <session_id> <cwd> <runtime> (text on stdin)"
|
|
298
|
+
)
|
|
299
|
+
return 2
|
|
300
|
+
try:
|
|
301
|
+
text = sys.stdin.read()
|
|
302
|
+
return capture_text_turn(args[1], text, args[2], args[3])
|
|
303
|
+
except Exception: # detached worker must die quietly
|
|
304
|
+
return 0
|
|
264
305
|
if len(args) < 2:
|
|
265
306
|
print(
|
|
266
307
|
"usage: python3 -m core.memory.turn_capture"
|
|
267
|
-
" (<session_id> <transcript_path> [cwd] |
|
|
308
|
+
" (<session_id> <transcript_path> [cwd] [runtime] |"
|
|
309
|
+
" capture-text <session_id> <cwd> <runtime> | maintenance)"
|
|
268
310
|
)
|
|
269
311
|
return 2
|
|
270
312
|
try:
|
|
271
|
-
return capture_turn(
|
|
313
|
+
return capture_turn(
|
|
314
|
+
args[0], args[1], args[2] if len(args) > 2 else "",
|
|
315
|
+
args[3] if len(args) > 3 else "claude",
|
|
316
|
+
)
|
|
272
317
|
except Exception: # detached worker must die quietly
|
|
273
318
|
return 0
|
|
274
319
|
|
|
@@ -9,6 +9,10 @@ gets the same governance stack as the Claude runtime:
|
|
|
9
9
|
pre_tool research gate + frontend gate (PreToolUse parity)
|
|
10
10
|
post_tool MCP usage telemetry (PostToolUse parity)
|
|
11
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
|
|
12
16
|
compact gate state context (PreCompact parity)
|
|
13
17
|
|
|
14
18
|
CLI contract::
|
|
@@ -23,7 +27,9 @@ same posture as the bash hooks it mirrors).
|
|
|
23
27
|
from __future__ import annotations
|
|
24
28
|
|
|
25
29
|
import json
|
|
30
|
+
import os
|
|
26
31
|
import re
|
|
32
|
+
import subprocess
|
|
27
33
|
import sys
|
|
28
34
|
from datetime import datetime, timezone
|
|
29
35
|
from pathlib import Path
|
|
@@ -115,11 +121,69 @@ def _store_prompt(session_id: str, prompt: str) -> None:
|
|
|
115
121
|
pass
|
|
116
122
|
|
|
117
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
|
+
|
|
118
180
|
def _action_prompt(payload: dict) -> dict:
|
|
119
|
-
"""Token-hygiene parity (config/hooks/token-hygiene.sh)
|
|
181
|
+
"""Token-hygiene parity (config/hooks/token-hygiene.sh) + the routing
|
|
182
|
+
injection (UserPromptSubmit parity) that makes /arka optional."""
|
|
120
183
|
prompt = str(payload.get("prompt", ""))
|
|
121
184
|
session_id = str(payload.get("session_id", ""))
|
|
122
185
|
suggestions: list[str] = []
|
|
186
|
+
routing = _routing_lines(prompt, session_id, str(payload.get("cwd", "")))
|
|
123
187
|
|
|
124
188
|
ctx = payload.get("context_pct")
|
|
125
189
|
if isinstance(ctx, (int, float)):
|
|
@@ -157,7 +221,7 @@ def _action_prompt(payload: dict) -> dict:
|
|
|
157
221
|
"ficheiro concreto."
|
|
158
222
|
)
|
|
159
223
|
|
|
160
|
-
return {"suggestions": suggestions}
|
|
224
|
+
return {"suggestions": suggestions, "routing": routing}
|
|
161
225
|
|
|
162
226
|
|
|
163
227
|
def _action_pre_tool(payload: dict) -> dict:
|
|
@@ -230,8 +294,38 @@ def _action_post_tool(payload: dict) -> dict:
|
|
|
230
294
|
return {"recorded_mcp": recorded}
|
|
231
295
|
|
|
232
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
|
+
|
|
233
326
|
def _action_idle(payload: dict) -> dict:
|
|
234
|
-
"""Stop-hook parity: kb-citation + [arka:meta] compliance (soft)
|
|
327
|
+
"""Stop-hook parity: kb-citation + [arka:meta] compliance (soft),
|
|
328
|
+
plus detached turn capture into the shared cross-runtime memory."""
|
|
235
329
|
text = str(payload.get("response_text", ""))
|
|
236
330
|
session_id = str(payload.get("session_id", ""))
|
|
237
331
|
nudges: list[str] = []
|
|
@@ -248,6 +342,8 @@ def _action_idle(payload: dict) -> dict:
|
|
|
248
342
|
"fecha com kb=N research=X persona=Y gap=Z critic=W."
|
|
249
343
|
)
|
|
250
344
|
|
|
345
|
+
_enqueue_turn_capture(session_id, text, str(payload.get("cwd", "")))
|
|
346
|
+
|
|
251
347
|
_append_jsonl(
|
|
252
348
|
_TELEMETRY_DIR / "opencode-compliance.jsonl",
|
|
253
349
|
{
|
|
@@ -261,6 +357,94 @@ def _action_idle(payload: dict) -> dict:
|
|
|
261
357
|
return {"nudges": nudges}
|
|
262
358
|
|
|
263
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
|
+
|
|
264
448
|
def _action_compact(payload: dict) -> dict:
|
|
265
449
|
"""PreCompact parity: carry gate state across compaction."""
|
|
266
450
|
from core.workflow import state
|
|
@@ -286,6 +470,7 @@ _ACTIONS = {
|
|
|
286
470
|
"pre_tool": _action_pre_tool,
|
|
287
471
|
"post_tool": _action_post_tool,
|
|
288
472
|
"idle": _action_idle,
|
|
473
|
+
"memory": _action_memory,
|
|
289
474
|
"compact": _action_compact,
|
|
290
475
|
}
|
|
291
476
|
|
|
@@ -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
|