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.
Files changed (54) hide show
  1. package/README.md +1 -1
  2. package/THE-ARKAOS-GUIDE.md +1 -1
  3. package/VERSION +1 -1
  4. package/arka/SKILL.md +1 -1
  5. package/config/skills-curated.yaml +2 -2
  6. package/config/skills-provenance.yaml +7 -0
  7. package/core/hooks/session_start.py +17 -3
  8. package/core/memory/semantic_store.py +58 -11
  9. package/core/memory/turn_capture.py +56 -11
  10. package/core/runtime/opencode_hooks.py +188 -3
  11. package/departments/dev/skills/animated-website/SKILL.md +4 -2
  12. package/departments/dev/skills/scroll-world/SKILL.md +619 -0
  13. package/departments/dev/skills/scroll-world/references/index-template.html +73 -0
  14. package/departments/dev/skills/scroll-world/references/knockout.py +89 -0
  15. package/departments/dev/skills/scroll-world/references/pipeline.md +213 -0
  16. package/departments/dev/skills/scroll-world/references/prompts.md +149 -0
  17. package/departments/dev/skills/scroll-world/references/scroll-world.LICENSE +21 -0
  18. package/departments/dev/skills/scroll-world/references/scrub-engine.js +448 -0
  19. package/harness/codex/AGENTS.md +1 -1
  20. package/harness/copilot/copilot-instructions.md +1 -1
  21. package/harness/cursor/rules/arkaos.mdc +2 -2
  22. package/harness/gemini/GEMINI.md +1 -1
  23. package/harness/opencode/AGENTS.md +1 -1
  24. package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
  25. package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
  26. package/harness/opencode/agents/arka-cfo-helena.md +1 -1
  27. package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
  28. package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
  29. package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
  30. package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
  31. package/harness/opencode/agents/arka-coo-sofia.md +1 -1
  32. package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
  33. package/harness/opencode/agents/arka-cqo-marta.md +1 -1
  34. package/harness/opencode/agents/arka-cto-marco.md +1 -1
  35. package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
  36. package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
  37. package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
  38. package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
  39. package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
  40. package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
  41. package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
  42. package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
  43. package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
  44. package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
  45. package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
  46. package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
  47. package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
  48. package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
  49. package/harness/opencode/plugins/arka.ts +15 -0
  50. package/harness/zed/.rules +1 -1
  51. package/installer/assets/opencode/arka.ts +15 -0
  52. package/knowledge/skills-manifest.json +14 -1
  53. package/package.json +1 -1
  54. package/pyproject.toml +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **The Operating System for AI Agent Teams.**
4
4
 
5
- 89 agents. 17 departments. 331 skills. Enterprise frameworks. Multi-runtime. One install.
5
+ 89 agents. 17 departments. 332 skills. Enterprise frameworks. Multi-runtime. One install.
6
6
 
7
7
  ```bash
8
8
  npx arkaos install
@@ -1,6 +1,6 @@
1
1
  # The ArkaOS Guide
2
2
 
3
- > v4.36.0 — 89 agents, 17 departments, 331 skills, 297 commands, 17 ADRs.
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.37.0
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. 331 skills. Multi-runtime. Dashboard. Knowledge RAG.
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, security-audit, spec,
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
- lines.append(f"[SESSION-MEMORY] - {record.ts[:10]}: {summary}")
349
- if len(lines) == 1:
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: {len(lines) - 1} turns ({project}),"
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
- "CREATE INDEX IF NOT EXISTS idx_turns_project_ts "
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, project_name,"
143
- " cwd, summary, tools_used, file_paths, importance, embedding,"
144
- " embedding_backend, embedding_model, dims)"
145
- " VALUES (:id, :ts, :session_id, :project_name, :cwd, :summary,"
146
- " :tools_used, :file_paths, :importance, :embedding,"
147
- " :embedding_backend, :embedding_model, :dims)",
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 capture_turn(session_id: str, transcript_path: str, cwd: str = "") -> int:
194
- if not _config_enabled() or not session_id:
195
- return 0
196
- raw = _read_tail(transcript_path)
197
- if not raw:
198
- return 0
199
- text = _last_assistant_text(transcript_path, raw)
200
- tools, paths = _parse_tool_uses(raw)
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] | maintenance)"
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(args[0], args[1], args[2] if len(args) > 2 else "")
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>; regular animated site WITHOUT a video
12
- source -> frontend-design skill; video editing/production -> content/video-produce.
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