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.
Files changed (59) 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.py +16 -5
  11. package/core/runtime/opencode_hooks.py +498 -0
  12. package/departments/dev/skills/animated-website/SKILL.md +4 -2
  13. package/departments/dev/skills/scroll-world/SKILL.md +619 -0
  14. package/departments/dev/skills/scroll-world/references/index-template.html +73 -0
  15. package/departments/dev/skills/scroll-world/references/knockout.py +89 -0
  16. package/departments/dev/skills/scroll-world/references/pipeline.md +213 -0
  17. package/departments/dev/skills/scroll-world/references/prompts.md +149 -0
  18. package/departments/dev/skills/scroll-world/references/scroll-world.LICENSE +21 -0
  19. package/departments/dev/skills/scroll-world/references/scrub-engine.js +448 -0
  20. package/harness/codex/AGENTS.md +1 -1
  21. package/harness/copilot/copilot-instructions.md +1 -1
  22. package/harness/cursor/rules/arkaos.mdc +2 -2
  23. package/harness/gemini/GEMINI.md +1 -1
  24. package/harness/opencode/AGENTS.md +1 -1
  25. package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
  26. package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
  27. package/harness/opencode/agents/arka-cfo-helena.md +1 -1
  28. package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
  29. package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
  30. package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
  31. package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
  32. package/harness/opencode/agents/arka-coo-sofia.md +1 -1
  33. package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
  34. package/harness/opencode/agents/arka-cqo-marta.md +1 -1
  35. package/harness/opencode/agents/arka-cto-marco.md +1 -1
  36. package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
  37. package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
  38. package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
  39. package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
  40. package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
  41. package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
  42. package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
  43. package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
  44. package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
  45. package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
  46. package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
  47. package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
  48. package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
  49. package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
  50. package/harness/opencode/agents-meta.json +27 -0
  51. package/harness/opencode/opencode.json +23 -0
  52. package/harness/opencode/plugins/arka.ts +128 -0
  53. package/harness/zed/.rules +1 -1
  54. package/installer/adapters/opencode.js +142 -16
  55. package/installer/assets/opencode/arka.ts +128 -0
  56. package/knowledge/skills-manifest.json +14 -1
  57. package/package.json +1 -1
  58. package/pyproject.toml +1 -1
  59. package/scripts/harness_gen.py +43 -3
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.36.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
 
@@ -2,9 +2,13 @@
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``), and AGENTS.md
6
- instructions. The installer adapter (installer/adapters/opencode.js)
7
- deploys the generated harness bundle onto those surfaces.
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).
8
12
 
9
13
  Headless invocation (live-verified against the installed binary,
10
14
  opencode 1.18.4, ``opencode run --help`` + a real probe, 2026-07-23):
@@ -60,18 +64,25 @@ class OpenCodeAdapter(RuntimeAdapter):
60
64
  config_dir=config_dir,
61
65
  skills_dir=config_dir / "commands",
62
66
  settings_file=config_dir / "opencode.json",
63
- supports_hooks=False,
67
+ supports_hooks=(config_dir / "plugins" / "arka.ts").exists(),
64
68
  supports_subagents=True,
65
69
  supports_mcp=True,
66
70
  max_context_tokens=200_000,
67
71
  )
68
72
 
69
73
  def capabilities(self) -> dict[str, bool]:
74
+ plugin = (
75
+ Path(expanduser("~")) / ".config" / "opencode" / "plugins" / "arka.ts"
76
+ )
70
77
  return {
71
78
  "agent_dispatch": False, # native agents exist; no ArkaOS wiring yet
72
79
  "headless": True, # `opencode run` live-verified 2026-07-23
73
80
  "file_ops": True, # terminal agent edits files natively
74
- "hooks": False, # plugins exist; no PreToolUse/Stop parity
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(),
75
86
  }
76
87
 
77
88
  def inject_context(self, layers: dict[str, str]) -> str: