Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/hooks/core/serena-primer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export function primerContext(): string {
"- Code discovery / reading: `get_symbols_overview`, `find_symbol`, `find_referencing_symbols`, `search_for_pattern`.",
"- Code edits: `replace_symbol_body`, `insert_after_symbol`, `insert_before_symbol`, `replace_content`.",
"- Native grep/glob: only for initial filename/path discovery. Do not fall back to grep + Read for code navigation just because Serena's tools aren't loaded yet — load them.",
'- Result size: omit `max_answer_chars` on Serena tools (uses the configured default, typically 150000). Never pass small caps like `3000` on broad searches. If a call returns "The answer is too long (N characters)", retry with `max_answer_chars` > N or narrow path/glob — do not keep the low cap.',
"- Exception — MCP timeout: if a Serena MCP call times out or hangs (seen mainly in OpenCode Desktop's long-lived sidecar), stop retrying MCP for this session: use native search/read for code, and access `.serena/memories/` files directly (or `serena memories read|write` when Serena CLI ≥ 1.5 is installed) for memory work. A full app relaunch restores Serena MCP.",
].join("\n");
}
Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/_version.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": "11.1.1",
"version": "11.6.0",
"schemaVersion": 2,
"mode": "project",
"installedAt": "2026-07-27T12:13:46.129Z"
"installedAt": "2026-08-03T12:14:26.889Z"
}
3 changes: 2 additions & 1 deletion .agents/skills/oma-frontend/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ Then run the project's frontend verification commands, typically lint, typecheck
5. Run the execution checklist before handoff and include relevant verification results.
6. **Self-describing file names**: every new file follows the File Naming convention in `../../rules/frontend.md` §Naming Conventions — domain + role readable from the basename alone (`order-summary-card.tsx`, `use-order-polling.ts`, `cart.atoms.ts`). Grab-bag names (`utils.ts`, `helpers.ts`, `misc.ts`) and version suffixes (`*-v2`, `*-final`) are banned.
7. **Next.js 16 `proxy.ts` is mandatory; `middleware.ts` is BANNED**: this project is Next.js 16+. `middleware.ts` is NOT "deprecated"; it is forbidden, touch it and you die. The canonical request-proxy / auth-gate file is `proxy.ts` (root or `src/`) exporting a `proxy` function. NEVER create, recommend, suggest, or "restore" `middleware.ts`. NEVER flag `proxy.ts` as dead code, unused, or not-wired. Any such finding is a fatal self-error: retract it immediately and write `proxy.ts`.
8. **Angular projects follow `resources/angular-rules.md`**: standalone components + `OnPush` + signals-first, `inject()` DI, lazy routes, new control flow. **Any non-trivial RxJS pipeline MUST ship with a marble test (`TestScheduler` from `rxjs/testing`)** — a stream without a marble test fails review. React/Next.js-specific rules (shadcn workflow, `proxy.ts`, Libraries table below) do not apply in Angular projects.
8. **`next/link` defaults to `prefetch={false}`**: every `<Link>` MUST pass `prefetch={false}` unless there is a stated reason not to. Next.js's default prefetching fires a request per link entering the viewport, which hammers container CPU/memory and origin bandwidth on list-heavy or nav-heavy pages. Opt back in (`prefetch` omitted, or `prefetch` / `prefetch="unstable_forceStale"`) ONLY for a small, deliberate set of high-intent targets (primary CTA, next step in a funnel), and note the reason inline. A `<Link>` without an explicit prefetch decision fails review.
9. **Angular projects follow `resources/angular-rules.md`**: standalone components + `OnPush` + signals-first, `inject()` DI, lazy routes, new control flow. **Any non-trivial RxJS pipeline MUST ship with a marble test (`TestScheduler` from `rxjs/testing`)** — a stream without a marble test fails review. React/Next.js-specific rules (shadcn workflow, `proxy.ts`, Libraries table below) do not apply in Angular projects.

### Libraries

Expand Down
1 change: 1 addition & 0 deletions .agents/skills/oma-frontend/resources/checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Run through every item before submitting your work.
- [ ] Do NOT flag `src/proxy.ts` as dead code or recommend renaming to `middleware.ts`; `proxy.ts` is the canonical Next.js 16+ convention
<!-- oma-docs:ignore-end -->
- [ ] Config flags use the `Proxy` form (e.g. `skipProxyUrlNormalize`), not the legacy `Middleware` form
- [ ] Every `<Link>` passes `prefetch={false}`; any prefetching link is a deliberate high-intent target with an inline reason

## TypeScript
- [ ] Strict mode, no `any` types
Expand Down
7 changes: 6 additions & 1 deletion .agents/skills/oma-frontend/resources/snippets.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ Copy-paste ready patterns. Use these as starting points, adapt to the specific t

```tsx
// Internal nav: <Link>, never <a href="/...">
// prefetch={false} is the DEFAULT — viewport prefetching eats container CPU/RAM.
import Link from "next/link";
<Link href="/gallery" className="...">View gallery</Link>
<Link href="/gallery" prefetch={false} className="...">View gallery</Link>

// Opt back in only for a few high-intent targets, and say why:
// primary funnel CTA — prefetch is intentional
<Link href="/checkout" className="...">Checkout</Link>

// Custom font: next/font, never <link rel="stylesheet">
import { Inter } from "next/font/google";
Expand Down
12 changes: 9 additions & 3 deletions .agents/skills/oma-video/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ Before invoking `oma video generate`, the calling agent runs this checklist. **I
- [ ] **Locale**: narration + caption language (default from config; translated via oma-translator when non-source).
- [ ] **Captions**: `tiktok` (centered, static windowed cues), `lower-third`, or `none`.
- [ ] **Duration**: target seconds (<= 180) or `auto` (derived from the script).
- [ ] **Voice / music**: voice profile or `none`; music `upbeat` / `calm` / `none`. **The default voice is `none` → a silent video with estimated caption timing.** Pass `--voice <profile>` (a Voicebox profile) whenever narration is expected. Music mixing is currently deferred (recorded + warned, not mixed).
- [ ] **Voice / music**: voice profile or `none`; music `upbeat` / `calm` / `none`. **The default voice is `none` → a silent video with estimated caption timing.** Pass `--voice <profile>` (a Voicebox profile) whenever narration is expected. Music is rendered offline by Strudel and mixed at −18 dB; it needs a one-time `oma video doctor --install-strudel` and degrades to no music without it.

**Amplification shortcut.** For a one-line brief (e.g. "shorts about Jeju coffee"), do not pop a questionnaire if the request is genuinely simple. Instead **amplify inline and show the user** the inferred plan before invoking:

Expand Down Expand Up @@ -247,7 +247,7 @@ oma video generate "<brief>" [--mode shorts|explainer|demo] \
[--aspect 9:16|16:9|1:1|auto] [--locale <lang>] \
[--captions tiktok|lower-third|none] \
[--visual auto|generate|stock|aigc|slide] \
[--voice <profile>|none] [--music upbeat|calm|none] \
[--voice <profile>|none] [--music upbeat|calm|cinematic|lofi|piano|none] \
[--duration <sec>|auto] [--compositor remotion|mpt] \
[--capture <path>] \
[--source file|web] [--url <url>] [--device <name>] \
Expand Down Expand Up @@ -298,7 +298,13 @@ Other skills call `oma video generate --format json` and parse the JSON envelope
- **Narration is one wav**: oma-voice joins every scene line into a single `audio/narration-01.wav`, referenced by `render-spec.audio.narration`. There are no per-scene `narration-NN.wav` files.
- **Timing**: per-line offsets live in `timing.json` (voicebox-stt -> estimated; the `tts-native` and `whisper-cpp` source values are reserved but not yet wired — `TODO(oma-deferred): whisper-cpp`); scene boundaries and caption cues are derived from it.
- **Captions**: key-free `.srt` (+ `.vtt`) built from `timing.json`; `render-spec.captions.file` points at the `.srt`. The compositor renders **static windowed cues** — the cue active at the current frame, CSS-wrapped (no per-word animation).
- **Music**: deferred (`TODO(oma-deferred): music`) — no music asset source is wired yet, so a requested `--music` mode is recorded in `script.json` and surfaced as a warning, but no music is mixed. When implemented, it mixes under narration at `render-spec.audio.musicGainDb` (default −18 dB).
- **Music**: `--music <preset>` renders a BGM bed with **Strudel** and mixes it under narration at `render-spec.audio.musicGainDb` (default −18 dB). The bed is generated offline (headless Chrome + `OfflineAudioContext`), so it needs no key, no network, and no audio device — a 30s bed renders in well under a second.
- **Presets**: `calm` (sustained pad + arpeggio), `upbeat` (bright plucks), `cinematic` (drone build to a lead), `lofi` (warm chords, swung ticks), `piano` (neoclassical arpeggio). Each preset picks its key and mode from the run `seed`, so the same preset sounds different run to run without a second pattern.
- **Artifacts** in the run dir: `music/bgm.wav` (mixed by the compositor), `music/bgm.mp3` (preview), `music/bgm-raw.wav` (pre-master), and `music/pattern.strudel` — the source that produced them, editable and re-renderable by hand.
- **Level**: every bed is normalised to −14 LUFS with a static gain before a peak limiter, so `musicGainDb` means the same thing for every preset. Normalisation is deliberately *not* `loudnorm`'s one-pass mode, which flattens the arrangement arc.
- **Opt-in install**: `@strudel/*` is AGPL-3.0-or-later while the oma CLI is MIT, so the deps are never bundled and never installed implicitly. Run `oma video doctor --install-strudel` once. The CLI never imports Strudel — it spawns `resources/strudel/render.mjs` as a subprocess, the same boundary the Remotion / Playwright projects use.
- **Fallback**: a missing install, a missing Chrome, or a failed render degrades to *no music* with a warning. The run still succeeds and `audio.music` stays unset (never a dangling `staticFile()` ref).
- **Determinism**: the built-in beds are oscillator-only (sine / triangle / square / sawtooth), which render byte-identically on replay. Noise sounds (`white` / `pink` / `brown`) draw from `Math.random()` and would break that, so the templates avoid them.

## References

Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/oma-video/resources/execution-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ plan when the brief is a one-liner.
- `mode` ∈ {`shorts`, `explainer`, `demo`}.
- `aspect` ∈ {`9:16`, `16:9`, `1:1`, `auto`} (`auto` snaps to the mode default: shorts -> 9:16, explainer/demo -> 16:9).
- `captions` ∈ {`tiktok`, `lower-third`, `none`}; `visual` ∈ {`auto`, `generate`, `stock`, `aigc`, `slide`}.
- `music` ∈ {`upbeat`, `calm`, `none`}; `compositor` ∈ {`remotion`, `mpt`}.
- `music` ∈ {`upbeat`, `calm`, `cinematic`, `lofi`, `piano`, `none`}; `compositor` ∈ {`remotion`, `mpt`}.
- `duration` ≤ `limits.max_duration_sec` (180); resulting `scenes` ≤ `limits.max_scenes` (40).
- `out` is inside `$PWD` unless `--allow-external-out`.
- For `demo`: `--capture` (if given) exists, is absolute + `$PWD`-guarded, and is a valid video format.
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/oma-video/resources/prompt-tips.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Scene/backdrop → Subject → Details → Constraints
- Anchor the **arc**: hook → body → payoff. A short without a hook gets swiped past.
- Match **aspect to mode** (9:16 shorts, 16:9 explainer/demo) or use `auto`.
- Keep narration **per-scene and short** so caption pages and scene boundaries align.
- Pick **music** that matches pacing (`upbeat` for shorts, `calm` for explainer) — note music mixing is currently deferred: the choice is recorded in `script.json` and warned, but not yet audible.
- Pick **music** that matches pacing (`upbeat` for shorts, `calm` for explainer) — Strudel renders the bed offline and mixes it at −18 dB under narration. Needs a one-time `oma video doctor --install-strudel`; without it the run still succeeds, just silent.

## Don'ts

Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/oma-video/resources/script-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ required** — omitting it is the most common authoring failure.
| `locale` | string (min 1) | ✅ | narration/caption language tag, e.g. `en`, `ko` |
| `title` | string (min 1) | ✅ | also drives the output filename slug: `<mode>-<slug>.mp4` |
| `scenes` | array (min 1, ≤ 40) | ✅ | see per-scene fields below |
| `music` | `upbeat` \| `calm` \| `none` | ✅ | recorded only — music mixing is deferred (`TODO(oma-deferred): music`) |
| `music` | `upbeat` \| `calm` \| `cinematic` \| `lofi` \| `piano` \| `none` | ✅ | drives the Strudel BGM bed; mixed at −18 dB under narration |
| `brand` | object | — | free-form; defaults to `{}` |

### Per-scene fields (`scenes[]`)
Expand Down Expand Up @@ -77,5 +77,5 @@ required** — omitting it is the most common authoring failure.
- [ ] `aspect` is concrete (`9:16` / `16:9` / `1:1`) — never `auto`.
- [ ] Scene count ≤ 40, total `durationSec` ≤ 180.
- [ ] Every scene has a `visual.kind` from the enum; `still` scenes carry an English `visual.prompt`.
- [ ] `music` is set (use `"none"` unless the user asked — mixing is deferred either way).
- [ ] `music` is set (use `"none"` unless the user asked for a bed).
- [ ] Validate cheaply before rendering: pass the file via `--script` with `--dry-run` first.
11 changes: 11 additions & 0 deletions .agents/skills/oma-video/resources/strudel/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "oma-video-strudel",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Vendored Strudel BGM renderer for oma-video. Deps are installed on demand by `oma video doctor --install-strudel`. @strudel/* is AGPL-3.0-or-later and is NEVER imported by the MIT-licensed oma CLI — render.mjs runs as a subprocess (same boundary as the Remotion / Playwright projects).",
"license": "MIT",
"dependencies": {
"@strudel/web": "^1.3.0"
}
}
87 changes: 87 additions & 0 deletions .agents/skills/oma-video/resources/strudel/page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>oma-video strudel offline render</title>
</head>
<body>
<!-- Served by render.mjs from the resolved @strudel/web dist. The IIFE
bundle exposes everything on `window.strudel` + `window.initStrudel`. -->
<script src="/strudel.js"></script>
<script>
window.__omaLog = [];
window.addEventListener('error', (e) =>
window.__omaLog.push('error: ' + e.message),
);
window.addEventListener('unhandledrejection', (e) =>
window.__omaLog.push('rejection: ' + (e.reason?.message ?? e.reason)),
);

/**
* Render `code` to a WAV and stash it on window as base64.
*
* `strudel.renderPatternAudio()` is a real OfflineAudioContext render
* (faster than realtime, no audio device involved) but it delivers the
* finished wav as a browser DOWNLOAD. Rather than plumb a download
* directory through CDP, we intercept URL.createObjectURL to capture the
* Blob in-page and hand the bytes back over the debugger protocol.
*
* Returns the base64 length; the driver pulls window.__omaWav in chunks.
*/
window.__omaRender = async function (code, seconds, cps, sampleRate) {
await window.initStrudel();

// repl.evaluate(code, autostart=false) transpiles + evaluates without
// starting the realtime scheduler, and returns the Pattern itself.
const evaluated = await window.strudel.evaluate(code, false);
const pattern = evaluated?.pattern ?? evaluated;
if (!pattern || typeof pattern.queryArc !== 'function') {
throw new Error(
'strudel: pattern code did not evaluate to a Pattern (got ' +
typeof pattern +
')',
);
}

let captured = null;
const realCreate = URL.createObjectURL.bind(URL);
const realRevoke = URL.revokeObjectURL.bind(URL);
URL.createObjectURL = (blob) => {
captured = blob;
return 'blob:oma-captured';
};
URL.revokeObjectURL = () => {};
try {
await window.strudel.renderPatternAudio(
pattern,
cps,
0,
seconds * cps,
sampleRate,
128,
false,
'oma-bgm',
);
} finally {
URL.createObjectURL = realCreate;
URL.revokeObjectURL = realRevoke;
}
if (!captured) {
throw new Error('strudel: renderPatternAudio produced no audio blob');
}

const bytes = new Uint8Array(await captured.arrayBuffer());
let binary = '';
const CHUNK = 0x8000; // stay under the String.fromCharCode arg limit
for (let i = 0; i < bytes.length; i += CHUNK) {
binary += String.fromCharCode.apply(
null,
bytes.subarray(i, i + CHUNK),
);
}
window.__omaWav = btoa(binary);
return window.__omaWav.length;
};
</script>
</body>
</html>
Loading