arkaos 5.17.0 → 5.17.1

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 (49) hide show
  1. package/README.md +2 -2
  2. package/THE-ARKAOS-GUIDE.md +2 -2
  3. package/VERSION +1 -1
  4. package/arka/SKILL.md +1 -1
  5. package/bin/arka-doctor +14 -8
  6. package/core/hooks/session_start.py +0 -26
  7. package/core/jobs/auto_doc_worker.py +34 -127
  8. package/core/synapse/layers.py +92 -2
  9. package/departments/content/SKILL.md +1 -0
  10. package/departments/content/skills/hyperframes/SKILL.md +95 -0
  11. package/departments/content/skills/video-produce/SKILL.md +27 -17
  12. package/departments/content/skills/video-setup/SKILL.md +26 -13
  13. package/harness/codex/AGENTS.md +2 -2
  14. package/harness/copilot/copilot-instructions.md +2 -2
  15. package/harness/cursor/rules/arkaos.mdc +3 -3
  16. package/harness/gemini/GEMINI.md +2 -2
  17. package/harness/opencode/AGENTS.md +2 -2
  18. package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
  19. package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
  20. package/harness/opencode/agents/arka-cfo-helena.md +1 -1
  21. package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
  22. package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
  23. package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
  24. package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
  25. package/harness/opencode/agents/arka-coo-sofia.md +1 -1
  26. package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
  27. package/harness/opencode/agents/arka-cqo-marta.md +1 -1
  28. package/harness/opencode/agents/arka-cto-marco.md +1 -1
  29. package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
  30. package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
  31. package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
  32. package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
  33. package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
  34. package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
  35. package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
  36. package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
  37. package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
  38. package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
  39. package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
  40. package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
  41. package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
  42. package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
  43. package/harness/zed/.rules +2 -2
  44. package/installer/doctor.js +18 -6
  45. package/knowledge/commands-keywords.json +29 -2
  46. package/knowledge/commands-registry.json +41 -5
  47. package/knowledge/skills-manifest.json +14 -1
  48. package/package.json +1 -1
  49. package/pyproject.toml +1 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **The Operating System for AI Agent Teams.**
6
6
 
7
- 89 agents. 17 departments. 340 skills. Enterprise frameworks. Multi-runtime. One install.
7
+ 89 agents. 17 departments. 341 skills. Enterprise frameworks. Multi-runtime. One install.
8
8
 
9
9
  ```bash
10
10
  npx arkaos install
@@ -97,7 +97,7 @@ npx arkaos doctor # Health check
97
97
 
98
98
  ### Skill packs, à la carte
99
99
 
100
- The default install ships a curated core so your context window stays lean. Everything else lives in the ArkaOS plugin marketplace: 16 department packs with 266 skills, generated straight from the same sources the core uses. Inside Claude Code:
100
+ The default install ships a curated core so your context window stays lean. Everything else lives in the ArkaOS plugin marketplace: 16 department packs with 267 skills, generated straight from the same sources the core uses. Inside Claude Code:
101
101
 
102
102
  ```
103
103
  /plugin marketplace add andreagroferreira/arka-os
@@ -1,6 +1,6 @@
1
1
  # The ArkaOS Guide
2
2
 
3
- > v5.16.1 — 89 agents, 17 departments, 340 skills, 306 commands, 20 ADRs.
3
+ > v5.17.1 — 89 agents, 17 departments, 341 skills, 307 commands, 20 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
@@ -40,7 +40,7 @@ answers as a generic assistant.
40
40
  | `/arka` | 22 |
41
41
  | `/brand` | 14 |
42
42
  | `/community` | 15 |
43
- | `/content` | 20 |
43
+ | `/content` | 21 |
44
44
  | `/dev` | 42 |
45
45
  | `/do` | 1 |
46
46
  | `/ecom` | 18 |
package/VERSION CHANGED
@@ -1 +1 @@
1
- 5.17.0
1
+ 5.17.1
package/arka/SKILL.md CHANGED
@@ -63,7 +63,7 @@ complementary, not alternatives.
63
63
  # ArkaOS — Main Orchestrator
64
64
 
65
65
  > **The Operating System for AI Agent Teams**
66
- > 89 agents. 17 departments. 340 skills. Multi-runtime. Dashboard. Knowledge RAG.
66
+ > 89 agents. 17 departments. 341 skills. Multi-runtime. Dashboard. Knowledge RAG.
67
67
 
68
68
  ## ⛔ Evidence flow — 4 gates (NON-NEGOTIABLE)
69
69
 
package/bin/arka-doctor CHANGED
@@ -370,17 +370,23 @@ run_check "plugins" "warn" "$PLUGINS_DESC" "$PLUGINS_OK" "Run: bash install.sh"
370
370
 
371
371
  # ─── Check 17: Hyperframes Skills (video production) ──────────────────────
372
372
  # Parity with installer/doctor.js `hyperframes-skills` (PR-C2): same name,
373
- # same sentinel, warn-only video production is opt-in.
373
+ # same sentinels, same two roots (~/.claude/skills and ~/.agents/skills),
374
+ # warn-only — video production is opt-in.
374
375
  HF_OK=false
375
376
  HF_DESC="Hyperframes skills AUSENTE (video-as-code editing for /content video)"
376
- for hf_name in hyperframes hyperframes-core; do
377
- if [ -f "$HOME/.claude/skills/$hf_name/SKILL.md" ]; then
378
- HF_OK=true
379
- HF_DESC="Hyperframes skills installed ($hf_name)"
380
- break
381
- fi
377
+ for hf_root in "$HOME/.claude/skills" "$HOME/.agents/skills"; do
378
+ for hf_name in hyperframes hyperframes-core; do
379
+ if [ -f "$hf_root/$hf_name/SKILL.md" ]; then
380
+ HF_OK=true
381
+ # Static, matching installer/doctor.js `description` exactly: run_check
382
+ # splices $desc unescaped into the --json payload, so no path or
383
+ # name interpolation may reach it.
384
+ HF_DESC="Hyperframes skills installed (video-as-code editing for /content video)"
385
+ break 2
386
+ fi
387
+ done
382
388
  done
383
- run_check "hyperframes-skills" "warn" "$HF_DESC" "$HF_OK" "Run /content video-setup in Claude Code"
389
+ run_check "hyperframes-skills" "warn" "$HF_DESC" "$HF_OK" "Run /content video-setup in Claude Code, or: npx hyperframes skills update (core set; npx hyperframes skills check reports staleness)"
384
390
 
385
391
  # ─── Check 18: SQLite corrupt-store backups (self-heal F1-D1) ──────────────
386
392
  # Parity with installer/doctor.js `sqlite-corrupt-backups`: same name,
@@ -318,31 +318,6 @@ def _trigger_reorganizer(repo: str, config: dict) -> None:
318
318
  _spawn_detached([sys.executable, "-m", "core.cognition.reorganizer_cli"], repo)
319
319
 
320
320
 
321
- def _trigger_auto_doc_drain(repo: str, config: dict) -> None:
322
- """Queue-aware AND config-gated: ``cognition.auto_doc_drain_on_session``
323
- (default True). The Stop hook enqueues auto-doc jobs; sessions drain
324
- them — the consumer follows the producer's cadence, so a machine with
325
- no sessions never wakes a worker for nothing (knowledge-flow PR1)."""
326
- cognition = config.get("cognition") or {}
327
- if not cognition.get("auto_doc_drain_on_session", True):
328
- return
329
- if not repo:
330
- return
331
- try:
332
- from core.jobs.auto_doc_worker import _queue_root
333
- if not any((_queue_root() / "pending").glob("*.json")):
334
- return
335
- except Exception:
336
- return
337
- max_age = cognition.get("auto_doc_max_age_days", 14)
338
- _spawn_detached(
339
- [sys.executable, "-m", "core.jobs.auto_doc_worker", "--once",
340
- "--max-jobs", "10", "--max-age-days", str(max_age)],
341
- repo,
342
- log_path=Path.home() / ".arkaos" / "logs" / "auto-doc-drain.log",
343
- )
344
-
345
-
346
321
  def _ensure_dashboard(repo: str, config: dict) -> None:
347
322
  dashboard = config.get("dashboard") or {}
348
323
  if not dashboard.get("ensure_on_session", True):
@@ -409,7 +384,6 @@ def build_visible(cwd: str) -> str:
409
384
  msg += _forge_line()
410
385
  msg += _drift(version)
411
386
  _trigger_reorganizer(repo, config)
412
- _trigger_auto_doc_drain(repo, config)
413
387
  _ensure_dashboard(repo, config)
414
388
  return msg
415
389
 
@@ -6,8 +6,6 @@ Queue layout under `~/.arkaos/jobs/auto-doc/`:
6
6
  processing/ — jobs currently being handled
7
7
  completed/ — jobs finished successfully
8
8
  failed/ — jobs that exhausted retries or could not be parsed
9
- expired/ — jobs older than --max-age-days, moved aside unprocessed
10
- (never deleted: the operator can audit or re-queue them)
11
9
 
12
10
  Each job is a JSON file:
13
11
 
@@ -28,31 +26,22 @@ second worker that tries to claim the same job will raise and skip it.
28
26
  from __future__ import annotations
29
27
 
30
28
  import argparse
31
- import contextlib
32
29
  import json
33
30
  import os
34
31
  import sys
35
32
  import time
36
33
  import uuid
37
- from datetime import UTC, datetime
34
+ from datetime import datetime, timezone
38
35
  from pathlib import Path
36
+ from typing import Optional
39
37
 
40
- from core.knowledge.vault import resolve_vault_path
41
38
  from core.shared import safe_session_id as _safe_session_id_module
42
39
 
40
+
43
41
  MAX_ATTEMPTS = 3
44
42
  # Re-export for backward compatibility with any external importers.
45
43
  SAFE_SESSION_ID_RE = _safe_session_id_module.SAFE_SESSION_ID_RE
46
- _QUEUE_SUBDIRS = ("pending", "processing", "completed", "failed", "expired")
47
-
48
- _VAULT_REMEDIATION = (
49
- "No vault configured. Set `knowledge.vaultPath` in ~/.arkaos/config.json "
50
- "(or export ARKAOS_VAULT). Jobs were left untouched in pending/."
51
- )
52
-
53
-
54
- class VaultNotConfiguredError(RuntimeError):
55
- """No vault answered; pending jobs must not be claimed or retried."""
44
+ _QUEUE_SUBDIRS = ("pending", "processing", "completed", "failed")
56
45
 
57
46
 
58
47
  def _queue_root() -> Path:
@@ -62,17 +51,11 @@ def _queue_root() -> Path:
62
51
  return Path.home() / ".arkaos" / "jobs" / "auto-doc"
63
52
 
64
53
 
65
- def _vault_path() -> Path | None:
66
- """The operator's vault via the canonical resolver, or ``None``.
67
-
68
- Resolution order is the canonical one from ``core.knowledge.vault``:
69
- ``knowledge.vaultPath`` in ``~/.arkaos/config.json`` first, then the
70
- ``ARKAOS_VAULT`` env var, both required to exist on disk. This
71
- replaces a hardcoded ``~/Documents/Personal`` fallback that silently
72
- sent every note to one developer's personal layout (the exact defect
73
- ``core.knowledge.vault`` was written to end — the worker predates it).
74
- """
75
- return resolve_vault_path()
54
+ def _vault_path() -> Path:
55
+ override = os.environ.get("ARKAOS_VAULT", "").strip()
56
+ if override:
57
+ return Path(override)
58
+ return Path.home() / "Documents" / "Personal"
76
59
 
77
60
 
78
61
  def _ensure_queue(root: Path) -> None:
@@ -81,7 +64,7 @@ def _ensure_queue(root: Path) -> None:
81
64
 
82
65
 
83
66
  def _now_iso() -> str:
84
- return datetime.now(UTC).isoformat()
67
+ return datetime.now(timezone.utc).isoformat()
85
68
 
86
69
 
87
70
  # ─── Enqueue ───────────────────────────────────────────────────────────
@@ -91,7 +74,7 @@ def enqueue_job(
91
74
  session_id: str,
92
75
  transcript_path: str,
93
76
  qg_verdict: str,
94
- queue_root: Path | None = None,
77
+ queue_root: Optional[Path] = None,
95
78
  ) -> str:
96
79
  """Write a pending job file. Returns the job id."""
97
80
  root = queue_root or _queue_root()
@@ -118,71 +101,22 @@ def enqueue_job(
118
101
 
119
102
  def process_pending_jobs(
120
103
  max_jobs: int = 10,
121
- queue_root: Path | None = None,
122
- max_age_days: float | None = None,
104
+ queue_root: Optional[Path] = None,
123
105
  ) -> list[dict]:
124
- """Process up to `max_jobs` pending jobs. Returns per-job result dicts.
125
-
126
- Raises :class:`VaultNotConfiguredError` BEFORE claiming anything when
127
- no vault is configured — a missing vault is an environment failure
128
- and must never consume job attempts or move files out of pending/.
129
- """
106
+ """Process up to `max_jobs` pending jobs. Returns per-job result dicts."""
130
107
  root = queue_root or _queue_root()
131
108
  _ensure_queue(root)
132
- vault = _vault_path()
133
- if vault is None:
134
- raise VaultNotConfiguredError(_VAULT_REMEDIATION)
135
- pending = sorted((root / "pending").glob("*.json"))
136
109
  results: list[dict] = []
137
- if max_age_days is not None:
138
- pending, expired = _partition_expired(pending, root, max_age_days)
139
- results.extend(expired)
110
+ pending = sorted((root / "pending").glob("*.json"))
140
111
  for job_file in pending[:max_jobs]:
141
112
  claimed = _claim(job_file, root)
142
113
  if claimed is None:
143
114
  continue
144
- results.append(run_single_job(claimed, root, vault=vault))
115
+ results.append(run_single_job(claimed, root))
145
116
  return results
146
117
 
147
118
 
148
- def _job_timestamp(job_file: Path) -> float | None:
149
- prefix = job_file.name.split("-", 1)[0]
150
- try:
151
- return float(prefix)
152
- except ValueError:
153
- return None
154
-
155
-
156
- def _partition_expired(
157
- pending: list[Path], root: Path, max_age_days: float
158
- ) -> tuple[list[Path], list[dict]]:
159
- """Split by job age, read from the unix prefix of the job filename.
160
-
161
- A name without a parsable prefix is kept for processing — unknown
162
- age must never silently shelve a job.
163
- """
164
- cutoff = time.time() - max_age_days * 86400.0
165
- keep: list[Path] = []
166
- expired: list[dict] = []
167
- for job_file in pending:
168
- stamp = _job_timestamp(job_file)
169
- if stamp is not None and stamp < cutoff:
170
- expired.append(_move_to_expired(job_file, root))
171
- else:
172
- keep.append(job_file)
173
- return keep, expired
174
-
175
-
176
- def _move_to_expired(job_file: Path, root: Path) -> dict:
177
- target = root / "expired" / job_file.name
178
- try:
179
- os.replace(job_file, target)
180
- except OSError:
181
- return {"job_id": job_file.stem, "status": "expire-failed"}
182
- return {"job_id": job_file.stem, "status": "expired"}
183
-
184
-
185
- def _claim(job_file: Path, root: Path) -> Path | None:
119
+ def _claim(job_file: Path, root: Path) -> Optional[Path]:
186
120
  target = root / "processing" / job_file.name
187
121
  try:
188
122
  os.replace(job_file, target)
@@ -193,17 +127,10 @@ def _claim(job_file: Path, root: Path) -> Path | None:
193
127
  return None
194
128
 
195
129
 
196
- def run_single_job(
197
- job_path: Path,
198
- queue_root: Path | None = None,
199
- vault: Path | None = None,
200
- ) -> dict:
130
+ def run_single_job(job_path: Path, queue_root: Optional[Path] = None) -> dict:
201
131
  """Process one job end-to-end. Moves to completed/ or failed/."""
202
132
  root = queue_root or _queue_root()
203
133
  _ensure_queue(root)
204
- vault = vault or _vault_path()
205
- if vault is None:
206
- return _release_claim(job_path, root)
207
134
  try:
208
135
  payload = json.loads(job_path.read_text(encoding="utf-8"))
209
136
  if not isinstance(payload, dict):
@@ -213,26 +140,18 @@ def run_single_job(
213
140
  payload["attempts"] = int(payload.get("attempts") or 0) + 1
214
141
  job_path.write_text(json.dumps(payload), encoding="utf-8")
215
142
  try:
216
- written = _execute(payload, vault)
143
+ written = _execute(payload)
217
144
  except Exception as exc:
218
145
  return _handle_retry(job_path, payload, root, str(exc))
219
146
  return _move_to_completed(job_path, root, payload, written)
220
147
 
221
148
 
222
- def _release_claim(job_path: Path, root: Path) -> dict:
223
- """Vault vanished between pre-flight and execution: put the job back
224
- WITHOUT touching attempts — environment failure, not job failure."""
225
- target = root / "pending" / job_path.name
226
- with contextlib.suppress(OSError):
227
- os.replace(job_path, target)
228
- return {"job_id": job_path.stem, "status": "vault-unconfigured"}
229
-
230
-
231
- def _execute(payload: dict, vault: Path) -> list[str]:
149
+ def _execute(payload: dict) -> list[str]:
232
150
  from core.cognition.auto_documentor import document_session
233
151
  transcript = Path(payload.get("transcript_path") or "")
234
152
  session_id = str(payload.get("session_id") or "")
235
153
  verdict = str(payload.get("qg_verdict") or "")
154
+ vault = _vault_path()
236
155
  paths = document_session(transcript, session_id, vault, verdict)
237
156
  return [str(p) for p in paths]
238
157
 
@@ -264,8 +183,10 @@ def _move_to_completed(
264
183
  payload["status"] = "completed"
265
184
  target = root / "completed" / job_path.name
266
185
  job_path.write_text(json.dumps(payload), encoding="utf-8")
267
- with contextlib.suppress(OSError):
186
+ try:
268
187
  os.replace(job_path, target)
188
+ except OSError:
189
+ pass
269
190
  return {
270
191
  "job_id": payload.get("job_id"),
271
192
  "status": "completed",
@@ -279,13 +200,17 @@ def _move_to_failed(job_path: Path, root: Path, error: str) -> dict:
279
200
  info["status"] = "failed"
280
201
  info["error"] = error
281
202
  info["failed_at"] = _now_iso()
282
- with contextlib.suppress(OSError):
203
+ try:
283
204
  job_path.write_text(json.dumps(info), encoding="utf-8")
205
+ except OSError:
206
+ pass
284
207
  try:
285
208
  os.replace(job_path, target)
286
209
  except OSError:
287
- with contextlib.suppress(OSError):
210
+ try:
288
211
  job_path.unlink()
212
+ except OSError:
213
+ pass
289
214
  return {
290
215
  "job_id": info.get("job_id"),
291
216
  "status": "failed",
@@ -314,38 +239,20 @@ def _build_parser() -> argparse.ArgumentParser:
314
239
  help="Cap on jobs per pass.")
315
240
  parser.add_argument("--interval", type=float, default=5.0,
316
241
  help="Seconds to sleep between passes when daemonised.")
317
- parser.add_argument("--max-age-days", type=float, default=None,
318
- help="Move pending jobs older than this to expired/ "
319
- "instead of processing them.")
320
242
  return parser
321
243
 
322
244
 
323
- def _run_pass(max_jobs: int, max_age_days: float | None) -> int | None:
324
- """One drain pass; prints results. Returns an exit code on fatal error."""
325
- try:
326
- results = process_pending_jobs(
327
- max_jobs=max_jobs, max_age_days=max_age_days
328
- )
329
- except VaultNotConfiguredError as exc:
330
- print(str(exc), file=sys.stderr)
331
- return 2
245
+ def main(argv: Optional[list[str]] = None) -> int:
246
+ args = _build_parser().parse_args(argv)
247
+ results = process_pending_jobs(max_jobs=args.max_jobs)
332
248
  for r in results:
333
249
  print(json.dumps(r), flush=True)
334
- return None
335
-
336
-
337
- def main(argv: list[str] | None = None) -> int:
338
- args = _build_parser().parse_args(argv)
339
- rc = _run_pass(args.max_jobs, args.max_age_days)
340
- if rc is not None:
341
- return rc
342
250
  if args.once:
343
251
  return 0
344
252
  while True:
345
253
  time.sleep(max(0.1, float(args.interval)))
346
- rc = _run_pass(args.max_jobs, args.max_age_days)
347
- if rc is not None:
348
- return rc
254
+ for r in process_pending_jobs(max_jobs=args.max_jobs):
255
+ print(json.dumps(r), flush=True)
349
256
 
350
257
 
351
258
  if __name__ == "__main__":
@@ -21,6 +21,7 @@ layers_kb.py. Both are re-exported here so existing
21
21
  `core.synapse.layers` imports keep working.
22
22
  """
23
23
 
24
+ import os
24
25
  import re
25
26
  import time
26
27
  from typing import Any
@@ -412,6 +413,24 @@ def _hub_skill(department: str) -> str:
412
413
  return _DEPT_HUB_EXCEPTIONS.get(department, f"arka-{department}")
413
414
 
414
415
 
416
+ # Project-shape signals: a cwd carrying ANY of these file groups (every
417
+ # file in a group present) forces the command's hint regardless of the
418
+ # prompt text — "working inside a Hyperframes project" is a route.
419
+ PROJECT_SIGNALS: dict[str, tuple[tuple[str, ...], ...]] = {
420
+ "content-hyperframes": (
421
+ ("hyperframes.json",),
422
+ ("BRIEF.md", "STORYBOARD.md"),
423
+ ),
424
+ }
425
+
426
+ # The score slot exists only so signal entries share the
427
+ # (score, command, department) tuple shape of _score_commands. Nothing
428
+ # sorts on it in the merge path: _hint_tag and compute discard the score,
429
+ # and precedence comes from _merge_hints concatenating signal entries
430
+ # before the keyword ones.
431
+ _SIGNAL_SCORE = 10_000
432
+
433
+
415
434
  def _hint_department(cmd: dict) -> str:
416
435
  """Department for the hint's hub skill.
417
436
 
@@ -426,8 +445,63 @@ def _hint_department(cmd: dict) -> str:
426
445
  return prefix.split("-")[0]
427
446
 
428
447
 
448
+ def _group_present(cwd: str, group: tuple[str, ...]) -> bool:
449
+ """True when EVERY file of the group sits directly in ``cwd``."""
450
+ return all(os.path.isfile(os.path.join(cwd, name)) for name in group)
451
+
452
+
453
+ def _project_signal_ids(cwd: str) -> list[str]:
454
+ """Registry command ids whose project shape matches the hook cwd.
455
+
456
+ Looks at ``cwd`` itself only — no parent walk — and never raises:
457
+ ``PromptContext.cwd`` is typed ``str``, and for str input the
458
+ ``os.path`` predicates (``isdir``/``isfile``) are exception-free —
459
+ they swallow OSError and ValueError internally — so an empty cwd, a
460
+ directory that does not exist, or an unreadable path yields no signal
461
+ and L5 degrades to plain keyword scoring. No try/except is needed.
462
+ """
463
+ if not cwd or not os.path.isdir(cwd):
464
+ return []
465
+ return [
466
+ cmd_id
467
+ for cmd_id, groups in PROJECT_SIGNALS.items()
468
+ if any(_group_present(cwd, group) for group in groups)
469
+ ]
470
+
471
+
472
+ def _signal_commands(
473
+ commands: list[dict[str, Any]], ids: list[str]
474
+ ) -> list[tuple[int, str, str]]:
475
+ """Scored tuples for the signalled ids, shaped like _score_commands.
476
+
477
+ An id the registry does not carry is skipped in silence: a signal
478
+ may only surface a command that actually exists, never invent one.
479
+ """
480
+ wanted = set(ids)
481
+ return [
482
+ (_SIGNAL_SCORE, cmd.get("command", ""), _hint_department(cmd))
483
+ for cmd in commands
484
+ if cmd.get("id") in wanted
485
+ ]
486
+
487
+
488
+ def _merge_hints(
489
+ signal: list[tuple[int, str, str]],
490
+ keyword: list[tuple[int, str, str]],
491
+ ) -> list[tuple[int, str, str]]:
492
+ """Signal hints first, keyword hints after, de-duped by command text."""
493
+ seen: set[str] = set()
494
+ merged: list[tuple[int, str, str]] = []
495
+ for entry in [*signal, *keyword]:
496
+ if entry[1] in seen:
497
+ continue
498
+ seen.add(entry[1])
499
+ merged.append(entry)
500
+ return merged
501
+
502
+
429
503
  def _score_commands(
430
- commands: list[dict], text: str
504
+ commands: list[dict[str, Any]], text: str
431
505
  ) -> list[tuple[int, str, str]]:
432
506
  """Keyword-score the registry commands against the prompt text,
433
507
  best score first; zero-score commands never qualify."""
@@ -468,6 +542,17 @@ class CommandHintsLayer(Layer):
468
542
  the route. The old `[hint:/cmd]` form was declarative and routinely
469
543
  ignored: the model announced the squad in prose and the skill's
470
544
  content never entered context.
545
+
546
+ Beyond the prompt words, the layer routes by PROJECT SHAPE: when the
547
+ hook cwd carries the files that identify a project kind
548
+ (``PROJECT_SIGNALS`` — e.g. a Hyperframes video-as-code project ships
549
+ ``hyperframes.json``, or ``BRIEF.md`` + ``STORYBOARD.md``), that
550
+ command is hinted even when the prompt matches no keyword. An
551
+ operator standing inside such a project asks "render the intro" or
552
+ "muda o segundo plano" — vocabulary the registry cannot enumerate —
553
+ and keyword-only scoring left the specialist route silent while the
554
+ generic assistant answered. Signal hints lead the merged list and the
555
+ top-2 cap still holds, so a signal costs at most one keyword slot.
471
556
  """
472
557
 
473
558
  def __init__(self, commands: list[dict] | None = None) -> None:
@@ -510,7 +595,12 @@ class CommandHintsLayer(Layer):
510
595
  cached=False,
511
596
  )
512
597
 
513
- top = _score_commands(self._commands, text)[:2]
598
+ signal = _signal_commands(
599
+ self._commands, _project_signal_ids(ctx.cwd)
600
+ )
601
+ top = _merge_hints(
602
+ signal, _score_commands(self._commands, text)
603
+ )[:2]
514
604
  hints = [command for _, command, _ in top]
515
605
 
516
606
  tags = _hint_tag(top)
@@ -38,6 +38,7 @@ allowed-tools: [Read, Write, Edit, Bash, Grep, Glob, Agent, WebFetch, WebSearch]
38
38
  | `/content calendar <period>` | Content calendar with themes and batching | Focused |
39
39
  | `/content thumbnail <video>` | Thumbnail + title packaging (A/B variants) | Specialist |
40
40
  | `/content video <topic>` | End-to-end video production (research → script → assets → render) | Enterprise |
41
+ | `/content hyperframes <task>` | Direct Hyperframes work: compose, animate, caption, port, render and review a video-as-code project | Specialist |
41
42
  | `/content trends <niche>` | Demand-first trend and niche analysis with STEPPS scoring | Specialist |
42
43
  | `/content research <topic>` | Sourced research compiled into a production brief | Focused |
43
44
  | `/content video-setup` | Video production environment bootstrap (Hyperframes, Agent-Reach, Higgsfield) | Specialist |
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: content/hyperframes
3
+ description: >
4
+ Direct Hyperframes work — video-as-code in HTML/CSS/JS + GSAP rendered to a
5
+ deterministic MP4 — on a new or existing project: compose, animate, keyframe,
6
+ caption, mix audio, port from Remotion, import from Figma, render, preview,
7
+ publish, batch-render. Loads the /hyperframes router FIRST and wraps it in the
8
+ ArkaOS doctrine (Simão owns it, [arka:design] marker on compositions,
9
+ dev/watch review of the render, Quality Gate). TRIGGER: "/content
10
+ hyperframes", "renderiza o vídeo", "render this composition", "ajusta os
11
+ keyframes", "adiciona legendas embutidas", "motion graphic", "title card",
12
+ "lower third", "converte este projeto Remotion", "importa do Figma para
13
+ vídeo", "slideshow", "faceless explainer", "explainer sem rosto", any cwd
14
+ holding hyperframes.json or BRIEF.md + STORYBOARD.md (Synapse L5 project
15
+ signal — inside a running /content video pipeline the pipeline keeps
16
+ ownership and calls this skill at its edit/render phase). SKIP: brief-to-MP4
17
+ production with research, script and asset generation -> content/video-produce
18
+ (its edit/render phase loads this skill); one-off environment install
19
+ -> content/video-setup; watching or analysing a video -> dev/watch;
20
+ scroll-driven website from a video -> dev/animated-website.
21
+ allowed-tools: [Read, Write, Edit, Bash, Grep, Glob, Agent, Skill, WebFetch]
22
+ metadata:
23
+ origin: arkaos
24
+ ---
25
+
26
+ <!-- arka:kb-first-prefix begin -->
27
+ > **KB-first:** query `mcp__obsidian__search_notes` (and
28
+ > `mcp__graphify__query_graph` when configured) and cite `[[wikilinks]]`
29
+ > or graph nodes — or declare the gap — BEFORE any external research.
30
+ > Full doctrine: `arka/SKILL.md` (KB-First Research).
31
+ <!-- arka:kb-first-prefix end -->
32
+
33
+ # Hyperframes — `/content hyperframes <task>`
34
+
35
+ > **Agent:** Simão (Video Producer) | **Engine:** Hyperframes by HeyGen (Apache-2.0) — core skills under `~/.claude/skills/`, creation workflows installed on demand by the router
36
+ > **Rule:** the `/hyperframes` router owns the workflow; this skill owns the ArkaOS ceremony around it. Never reconstruct Hyperframes from memory.
37
+
38
+ ## Phase 0 — Preflight (one command, always)
39
+
40
+ ```
41
+ npx hyperframes skills check
42
+ ```
43
+
44
+ | Result | Action |
45
+ |---|---|
46
+ | `N current`, nothing outdated | proceed |
47
+ | `↑ outdated` listed | `npx hyperframes skills update` refreshes the core set and every skill already installed; it never installs a workflow that is absent. The router installs that one when it picks it, with `npx hyperframes skills update <workflow>` |
48
+ | `~/.claude/skills/hyperframes/SKILL.md` missing | STOP with exactly "Hyperframes não instalado — corre /content video-setup". Never improvise the format. |
49
+ | `node --version` below 22, or no `ffmpeg` | STOP and print the platform install line — `content/video-setup` owns installs, this skill never mutates system packages |
50
+
51
+ ## Phase 1 — Load the router, then follow it
52
+
53
+ Invoke `Skill(hyperframes)` FIRST. It resumes project state (`BRIEF.md`, `hyperframes.json`, `STORYBOARD.md`), runs the intent interview for fresh creation, picks the owning workflow from its route table and installs it. Two ArkaOS constraints on top:
54
+
55
+ - A specific operation on an existing project (inspect, diagnose, validate, preview, render, publish, batch-render): the router loads `/hyperframes-cli` — do that operation and nothing else.
56
+ - Fresh creation: the interview ends by writing `BRIEF.md`. Read the marketing context first (KB-first: `WizardingCode/Marketing/product-marketing.md` in Obsidian, else the project-local `.agents/product-marketing.md`) so the brief carries the real positioning, never an invented one.
57
+
58
+ ## Phase 2 — Compose under the design doctrine
59
+
60
+ A Hyperframes composition is `.html`, so the frontend excellence gate applies exactly as it does to UI (constitution `excellence-mandate`, `core/workflow/frontend_gate.py`). Before the first Write/Edit of a composition emit the structured marker on a line of its own:
61
+
62
+ ```
63
+ [arka:design] benchmark=<named reference: Linear launch films, Vercel Ship keynote, Stripe Sessions…> skills=hyperframes,hyperframes-core,hyperframes-animation,<hyperframes-creative|hyperframes-keyframes|hyperframes-audio as loaded> tokens=<brand tokens path|none>
64
+ ```
65
+
66
+ - ArkaOS-branded output follows The Assembly v1.1 (`docs/design/brand-assembly/`, skill `arkaos-design`): zinc + rose, Signal used sparingly, Space Grotesk / IBM Plex — `tokens=` points there.
67
+ - Client work: `tokens=` points at the project's design-system document (`project-design-system-prerequisite`): no document, no composition — extract it first with `/brand design-system`.
68
+ - Anti-default: no template title cards, no stock easing on every element, no "AI video" look. GSAP timelines with labels and position parameters (Simão's framework): load `dev/gsap` for GSAP itself (`references/timeline.md` for timelines, labels and position parameters; `references/core.md` for tweens and easing; `references/plugins.md` for kinetic type and SplitText; `references/performance.md` before the render) — upstream `hyperframes-animation` covers the Hyperframes-specific timing; it does not replace it. Word-level captions through `/embedded-captions` whenever there is speech.
69
+ - Assets: `/media-use` resolves BGM, SFX, images and logos from licensed sources before anything is downloaded. Generated assets come from Higgsfield (`content/image-create`, `generate_video`); credits are metered — never regenerate in a loop without explicit approval.
70
+
71
+ ## Phase 3 — Render and review with evidence
72
+
73
+ 1. Render through the router / `/hyperframes-cli` (`npx hyperframes render …`). Keep the command and exit code on record (G3 of the evidence flow).
74
+ 2. Review the render with `dev/watch` on the output MP4: complete frames + timestamped transcript. Judge the hook (first 3 s), pacing, caption accuracy, audio sync, aspect ratio, brand fidelity. Screenshots are not evidence; frames are.
75
+ 3. Loop on defects. A reshoot the review demands is never blocked by time or token cost; the CostGovernor budget is the only ceiling.
76
+
77
+ ## Phase 4 — Quality Gate and delivery
78
+
79
+ Marta orchestrates Eduardo (on-screen copy, captions, description) and Francisca (render spec, determinism, composition code quality). Binary verdict. Deliver the master MP4, the per-platform reframes the brief asked for, the SRT when captions exist, and the `[arka:design]` line plus review notes in the delivery summary. Output goes to Obsidian `WizardingCode/Content/Video/<date>-<slug>/` (or the client project's vault path).
80
+
81
+ ## Never
82
+
83
+ - Never load `/hyperframes` when the preflight fails, and never guess what Hyperframes is.
84
+ - Never bypass the router's route table with a hand-picked workflow "because it looks right" — the router reads `references/routes/<workflow>.md` and decides.
85
+ - Never ship a render nobody watched.
86
+ - Never install Hyperframes skills from a marketplace other than `heygen-com/hyperframes`; third-party "video" skills that wrap paid engines or hook every `*video*` skill are rejected by decision (spec `hyperframes-routing`).
87
+
88
+ ## Examples
89
+
90
+ ```
91
+ /content hyperframes "renderiza o projeto em ./launch-film e revê o resultado"
92
+ /content hyperframes "adiciona legendas embutidas word-level ao talking head em ./ep03"
93
+ /content hyperframes "converte a composição Remotion em ./remotion-intro para Hyperframes"
94
+ /content hyperframes "motion graphic de 8 s com o número 30K a subir, brand ArkaOS"
95
+ ```