CLAUDE.md · diff

git:20260909.4eb49a2 to git:20260909.9d01f98

2 added, 0 removed. Audit A to A.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository Overview
oh-my-toong is a multi-AI skills and configuration management system. It defines skills, agents, hooks, and commands as source-of-truth components, then syncs them to target projects for Claude Code, Gemini CLI, and Codex CLI via a declarative `sync.yaml` format. Greek mythology naming convention.
## Development Commands
```bash
make validate # Schema + components + TypeScript typecheck
make validate-schema # YAML schema validation only
make validate-components # Referenced file/directory existence check
make validate-skill-refs # Skill path-reference validation
make typecheck # TypeScript strict type-check (tsc --noEmit)
make test # Run all tests (Shell + TypeScript); writes an untruncated log (path printed; override with OMT_TEST_LOG)
make sync-dry # Preview sync changes (no writes)
make sync # Deploy to target projects (requires default branch + clean tree; runs validate + tests first)
```
`make sync` refuses to run unless the current branch is this repo's default branch and the working tree is clean (no staged, unstaged, or untracked changes) — commit first, then sync. There is no supported env var or CLI flag to bypass this gate, and it should not be worked around. `make sync-dry` is exempt from this gate since it only previews and writes nothing. See `docs/sync-deploy-targets.md` for the full gate mechanics and known scope.
### Running Individual Tests
```bash
bash hooks/keyword-detector_test.sh # Single shell test (colocated next to source)
bun test tools/ # Sync orchestrator tests
bun test # All TypeScript tests
```
### Prerequisites
`bun`, `bash` (macOS 3.2 compatible), `node`/`npm` (including `npx`), `jq`, `sqlite3`
`jq` is a runtime prerequisite, not just a dev tool: the shipped hooks parse their
payloads with it. Most fail open when it is absent, but
`codex-spawn-context-gate.sh` and `codex-spawn-role-gate.sh` deny the call. macOS
15+ ships it in the base system at `/usr/bin/jq`; older macOS needs it installed.
`sqlite3` is a runtime prerequisite for Codex's active-ultragoal child detector. If
the binary, state database, query, or rollout data is unavailable or malformed,
`codex-persistent-mode` emits one diagnostic to stderr and fails open by counting
zero active children.
The root `sync.yaml` provisions the external Mermaid presentation renderer during
`make sync`. When `mmdc` is missing, sync runs
`npm i -g @mermaid-js/mermaid-cli`; it then renders a tiny flowchart to a temporary
SVG with the real `mmdc` command. Because `mmdc` uses Puppeteer's `headless: "shell"`
path by default, this smoke check also catches a missing or unusable
`chrome-headless-shell`. A failed smoke check runs
`npx --yes puppeteer browsers install chrome-headless-shell`.
Provision items remain ordered, per-target, dry-run-safe, and non-fatal: a failed
check or install is warned about and does not abort sync, while `make sync-dry`
only previews the intended checks and commands. The renderer scripts do not install
packages themselves. If a presentation renderer is run directly before a successful
sync, install the two external pieces manually:
```bash
npm i -g @mermaid-js/mermaid-cli
npx puppeteer browsers install chrome-headless-shell
```
The Mermaid render-gate hook fails open when `mmdc` is absent so ordinary markdown
writes are not wedged, but mandatory presentation renders still require both
`mmdc` and its headless shell.
## Architecture
### Directory Layout
```
oh-my-toong/
├── skills/ # Skill definitions (each: skills/<name>/SKILL.md)
├── agents/ # Subagent prompt definitions (<name>.md)
├── commands/ # Slash command definitions (<name>.md)
├── hooks/ # Session lifecycle scripts (sh/js/py)
├── rules/ # Behavioral rules synced as .claude/rules/
├── lib/ # Shared TypeScript helpers (ESM, bun:test)
├── scripts/ # Deployed script packages (hud, chunk-review)
├── tools/ # Internal sync/validation tooling (not deployed)
│ ├── adapters/ # Platform adapters (claude.ts, gemini.ts, codex.ts, opencode.ts)
│ └── lib/ # Shared TypeScript modules for sync tools
├── evals/ # Skill measurement records — harness + irreproducible baselines (not deployed)
├── projects/ # Project-specific overrides (skills, hooks per project)
├── config.yaml # Global defaults (use-platforms, feature-platforms, backup retention)
├── claude.yaml # Per-platform config (config/hooks/mcps/plugins)
├── gemini.yaml # Per-platform config (config/hooks/mcps)
├── codex.yaml # Per-platform config (config/hooks/mcps/model-map)
├── opencode.yaml # Per-platform config (config/mcps/model-map)
└── sync.yaml # Root sync definition (+ projects/*/sync.yaml per project)
```
### Sync System (Core Feature)
The sync tool (`tools/sync.ts`) reads `sync.yaml` files and deploys components to target project directories (`.claude/`, `.gemini/`, `.codex/`).
**Processing order**: `projects/*/sync.yaml` first (project-specific), then root `sync.yaml` (skips already-processed paths).
**sync.yaml format** (object with `items` array):
```yaml
path: /path/to/target/project
format: "pnpm exec prettier --write" # Optional: post-deploy format pass (see below)
agents:
items:
- oracle # String shorthand
- component: sisyphus-junior # Object with options
add-skills: [testing] # Inject skills into agent frontmatter
skills:
items:
- prometheus
- component: my-project:testing # Scoped: projects/my-project/skills/testing/
platforms: [claude] # Per-item platform override
```
**Platform resolution priority**: item-level > section-level > sync.yaml top-level > `config.yaml` feature-platforms > `config.yaml` use-platforms > hardcoded `[claude]`
**Component resolution** (scoped, upward search):
- Root `sync.yaml`: global paths only (`skills/`, `agents/`, etc.)
- Project `sync.yaml`: own project first (`projects/<name>/skills/`), then global fallback. Cross-project references are blocked.
**Post-deploy format** (top-level `format: "<command>"` or `format: ["<arg>", …]`): Optional. When declared, the sync tool runs this command once at each target after deploy, so deployed files land already in the target's own formatter normal form. See `docs/sync-deploy-targets.md`.
**Per-platform YAML** (`{platform}.yaml`): Colocated with `sync.yaml`, inheriting its `path`. Manages config/hooks/mcps/plugins per platform — separate from `sync.yaml` which handles component deployment only (agents, commands, skills, scripts, rules). For Claude, config/hooks deep-merge into the target's gitignored `.claude/settings.local.json` (global sync uses `settings.json`); a key set to `null` deletes that key (RFC 7386 JSON Merge Patch), while omission preserves existing state. Named MCP tombstones use `mcps.<name>: null`: Claude (root YAML at user scope, project YAML at that project's local MCP location), Codex, and OpenCode remove only the named server; Gemini rejects them. Claude plugin `{ name, state: absent }` uninstalls only that plugin at the matching user/project scope; omitted state remains `present`. A section-level `null` (`config:`/`hooks:`/`mcps:`) skips deployment for that run rather than deleting existing state. See `docs/platform-yaml-config-deployment.md` for platform destinations and the two-layer gitignore mechanism (why a personal absolute path is safe in `claude.yaml`, not just `claude.local.yaml`).
**Codex config ownership**: The default `.codex/config.toml` remains in use. Target-local `.omt/codex-config-state.json` tracks owned leaf paths and their last-applied TOML values (`valueToml`); comments, including old `omt:config` markers, do not establish ownership. Preexisting keys require explicit adoption. Omitted keys remain unchanged, explicit key-level `null` requests deletion, and conflicts preserve user settings. `make sync-dry` reads the real target and reports required adoption and conflicts without writing.
**Codex recovery and MCPs**: `.omt/codex-config-pending.json` journals config/state transitions so an interrupted operation can resume when the files match recognized before/after states. This does not provide universal compare-and-swap protection against other writers. Native Codex MCP add/remove changes are captured in the same transaction; omission preserves servers and `mcps.<name>: null` deletes only that named server. The existing `codex/mcps` names manifest remains in use. See [Platform YAML Configuration Deployment](docs/platform-yaml-config-deployment.md) for ownership, adoption, and recovery details.
> **Note**: `mcps/` directory is deprecated. MCPs are now defined inline in per-platform YAML files.
**Adapters** (`tools/adapters/`): Each platform has its own adapter that handles directory layout differences.
| Platform | Target dir | Supported categories | Notes |
|----------|-----------|---------------------|-------|
| claude | `.claude/` | agents, commands, skills, scripts, rules | Full native support |
| gemini | `.gemini/` | commands, skills, scripts | Hooks/config via syncPlatformYaml |
| codex | `.codex/` + `.agents/` | agents, skills, scripts, rules, hooks | TWO disjoint deploy roots: skills land in `.agents/skills/<name>`, everything else in `.codex/`. `deployLocationForManifest(platform, category)` (`tools/sync.ts`) is the single formula for that split. Agents: md→toml translate; Hooks/config via syncPlatformYaml |
| opencode | `.opencode/` | agents, commands, skills, scripts, rules | Hooks not supported |
### Core Skills
| Skill | Purpose | Key Constraint |
|-------|---------|----------------|
| prometheus | Strategic planning consultant | Planner only - NEVER implements |
| sisyphus | Task orchestrator | Delegates via subagents - orchestrates, doesn't solo |
| sisyphus-junior | Focused executor | Works ALONE - no delegation, strict todo discipline |
| momus | Work plan reviewer | Ruthlessly critical - catches gaps before implementation |
| diagnose | Architecture/debugging advisor | READ-ONLY consultant - diagnoses, never implements |
| clarify | Requirements clarification | MANDATORY gate before implementation |
| git-master | Git conventions (commits + branch naming) | Korean messages, 50-char limit, atomic commits |
| agent-council | Multi-AI advisory body | For trade-offs and subjective decisions |
| qa | Quality Assurance verification | Enforced actor-roster → story → cell → record → verdict → complete chain; runtime gates block unrecorded drivers and Stop; `record-cell` mechanically rejects a unit/integration test-runner report (`vitest`/`jest`/`pytest`/`go test` output) as scenario evidence (test logs live only in BASELINE); an undriven user boundary is `unverified`, never green; visual claim reviews bind observations to evidence hashes; completion requires a fresh inspected HTML receipt; STATE HTML report leads with a context-free-PO presentation layer (product/user perspective, anchored to recorded actors/stories/ACs; `presentation.md`) |
| explain-diff | Diff explanation ending in a comprehension quiz | Completion is the reader passing, not the document; evidence → background → goal → architecture → intuition → commits → code → render → quiz, goal states purpose+core before mechanism, Intuition examples continue through code and changed-input prediction questions, system level = cross-process boundaries, template-owned visuals (style invention rejected), no quiz exemption |
| agent-browser | Web and Electron E2E | Load this skill before using its driver CLI |
| agent-device | iOS, tvOS, macOS, Android, Vega OS TV E2E | Load this skill before using its driver CLI; delegates runtime help |
| dogfood | Mobile exploratory QA | Load this skill before using its driver CLI |
+ Ultragoal final-review consumers use scope-first admission: `OUT_OF_SCOPE` is a note and `UNKNOWN` blocks without repair. Findings retain `class`, `verdict`, and harm `impact`, and add `priority` (`HIGH|MEDIUM|LOW`) plus five nonblank assessment strings (`unfixed_cost`, `exposure`, `remedy`, `added_cost`, `rationale`). Verified HIGH requires repair, checks, and fresh review; MEDIUM requires repair, checks, and hash-bound COMMENT resolution; LOW is notes-only with no fix or fabricated evidence. Mixed MEDIUM/LOW resolves MEDIUM; LOW-only or excluded-only needs no resolution. Empty findings APPROVE. COMMENT/APPROVE do not trigger re-review, and objective/story gates cannot be waived by LOW.
+
### Hooks
- **Husky v9 lifecycle**: `package.json` declares `prepare: husky`; installation activates `.husky/_/` wrappers that route to the tracked plain `.husky/pre-commit` and `.husky/pre-push` files. `pre-commit` runs `bun run lint`; `pre-push` runs `bun run lint` followed by `make test`.
- **session-start.sh**: Restores persistent mode state and garbage-collects `$OMT_DIR` on session start; emits an active, non-pristine explain-diff restoration banner while excluding the pristine initial seed
- **orphan-reaper.sh**: SessionStart hook — reaps `code-review` finder worker process groups left behind when a conductor never reached teardown
- **hooks/lib/state-liveness.sh**: Shared TTL/liveness definitions for state-file and session-artifact garbage collection
- **scripts/omt-cleanup/**: `~/.omt` cleanup CLI, dry-run by default, `--execute` required to delete
- **keyword-detector.sh** / **codex-keyword-detector.sh**: Detects keywords (ultrawork/uw, think, search, analyze) and injects mode context (shared core)
- **label-commit-gate.sh** / **codex-label-commit-gate.sh**: Hard-blocks a commit whose message subject contains an invented/opaque label
- **label-edit-warn.sh** / **codex-label-edit-warn.sh**: Soft-warns (never blocks) when just-written content contains a bare invented/opaque label
- **local-path-ref-gate-core.sh** / **local-path-ref-gate.sh** / **codex-local-path-ref-gate.sh (Codex twin)**: Shared-core prevention gate for local paths sent through git commits, PR creates/edits/comments, Notion, Slack, or Linear; Claude/Codex shims use the same predicate and remedies. It fails open on missing tools, malformed payloads, unknown command/MCP shapes, or Git/setup inspection errors. Currently unregistered in `claude.yaml`/`codex.yaml` (quadratic-scaling tokenizer cost on large Bash calls/diffs); scripts are preserved pending re-optimization, tracked as a separate issue.
- **mermaid-render-gate.sh**: PostToolUse gate — renders every mermaid block of a just-written markdown file through `mmdc` (real mermaid inside headless Chromium, so layout-stage crashes surface too, not just parse errors) and blocks with the failing block located by file line number. Fails open when `mmdc` is absent; root `sync.yaml` provisions `mmdc` and Puppeteer's `chrome-headless-shell`, while mandatory presentation renderers report missing setup. Claude-only, no Codex twin
- **persistent-mode/** / **codex-persistent-mode/**: Prevents stopping when work remains incomplete (shared `makeDecision`); for an active ultragoal, consecutive no-progress Stops increment the iteration counter, observed diff-carrying commits or story transitions reset it, background-work waits are not counted, and the cap soft-stops as `budget_limited` for user-only `resume-pursuit` recovery. An active explain-diff session blocks Stop until the reader passes the quiz (or the state goes `stalled`, or a question is outstanding). For an active prometheus session, `<prometheus-done/>` is refused while a written plan (`steps.plan.done`) lacks a fresh Stage A presentation (shared predicate `stageAPresentationStatus` in `lib/state-core.ts`, same freshness rule as the state CLI's S6+ gate); pre-plan aborts and the block-count cap still tear down
- **pre-tool-enforcer.sh** / **codex-write-guard.sh**: PreToolUse gates — TaskOutput blocking, session-ledger write guard, code-review artifact identity guard, and user-only ultragoal-state command guard (`approve-review-dispatch-renewal` / `dismiss-review-finding` / `resume-pursuit` — each is user-authorized; the AI's Bash path is denied and the user runs them). Structured code reviews publish original review JSON through the generic bundled `scripts/submit-review.ts` publisher; callers own any aggregate or completion policy. Codex twin additionally denies dangerous commands (`rm -rf`, `git push --force`) and best-effort blocks ordinary direct writes/deletions in the current-session `codex-skill-invocation-marker-<sid>-*` namespace
- **review-dispatch-gate-core.sh** / **pre-tool-enforcer.sh** / **codex-review-dispatch-gate.sh**: Shared final-review dispatch budget — Claude/Codex shims atomically claim only active `phase=pursuing` `code-reviewer` dispatches; the initial five-dispatch window denies cap exhaustion or completion-eligible re-dispatch until explicit user approval renews the cap by 5.
- **qa-driver-guard.sh** / **codex-qa-driver-guard.sh**: QA PreToolUse driver guards — block `agent-device`/`agent-browser`/`curl`/`bash` while the roster is incomplete or BASELINE+ has an incomplete chain (PLAN reachability probes remain available); consume `derived.driver_gate_armed` and fail open without `jq`
- **explain-diff-artifact-guard.sh** / **codex-explain-diff-artifact-guard.sh**: PreToolUse gates over `$OMT_DIR/explain-diff/` — the only INVERTED guards in OMT: absent, expired, inactive, or unreadable (`jq` missing) state all DENY, while every path outside that directory stays fail-open. Shared verdict + byte-identical deny JSON in `hooks/lib/explain-diff-guard-core.sh`; the directory-boundary match keeps the sibling `explain-diff-eval/` tree outside the gate
- **hooks/lib/skill-invocation-core.sh**: Shared skill metadata parser and project-local-then-global protected-skill resolver used by the Codex invocation hooks
- **codex-skill-invocation-marker.sh**: Codex UserPromptSubmit hook — resolves each explicit literal `$skill` mention to the nearest project-local, then global protected `SKILL.md`, and injects its full body as trusted `additionalContext`; also records a marker as an invocation audit/integrity record, never as authorization
- **codex-skill-invocation-gate.sh**: Codex PreToolUse gate — literal reads of model-disabled `SKILL.md` bodies are always denied, regardless of marker presence or forgery; uncertain command shapes fail open
- **codex-explain-diff-seed.sh**: Codex explain-diff invocation seed — arms the fail-closed artifact guard only on a `$explain-diff` prompt mention (prompt-only; opening a file does not seed it; Claude seeds the same skeleton from `pre-tool-enforcer.sh`'s Skill branch)
- **codex-qa-seed.sh**: Codex QA invocation seed — creates the qa state skeleton and arms the same runtime gates on Codex, which has no native Skill invocation signal
- **codex-spawn-depth-gate.sh**: Codex PreToolUse gate capping subagent spawn depth at 2 (Claude enforces the same cap natively via `claude.yaml`'s `CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH`)
- **codex-spawn-context-gate.sh**: Codex PreToolUse hook normalizing every native `spawn_agent` call reaching the hook to `fork_turns: "none"` and removing legacy `fork_context` through `updatedInput`, regardless of the active skill. Preserves input fields other than these context controls, leaves already-normalized calls unchanged, and denies malformed payloads or missing/failing `jq`; disabled hooks and runtime timeouts are outside this guarantee.
- **codex-spawn-role-gate.sh**: Codex PreToolUse gate requiring an explicit nonblank string `agent_type` for every native `spawn_agent` call reaching the hook. The generic `default` role is valid; missing, null, blank, or non-string values are denied without rewriting input. Unknown role names are validated by the native runtime. Malformed payloads or missing/failing `jq` are denied.
### Key Workflows
**Ultrawork Mode** (`ultrawork`, `ulw`, `uw` keywords):
- Maximum precision mode with parallel agent utilization
- Activated via keyword detection (1-time context injection per message)
## Coding Conventions
- **Bash**: `set -euo pipefail`, macOS Bash 3.2 compatible (no associative arrays, no `declare -A`), quote all variables
- **TypeScript**: ESM modules, bun:test for testing. No build step required.
- **YAML**: 2-space indentation
- **Naming**: `skills/<greek-name>/`, `agents/<name>.md`, `hooks/<purpose>.(sh|js|py)`
- **Shell tests**: Colocated next to source files with `_test.sh` suffix (e.g., `hooks/keyword-detector_test.sh`); use `mktemp -d` with cleanup
- **TypeScript tests**: Colocated next to source files with `.test.ts` suffix (e.g., `tools/sync.test.ts`); use bun:test
## Critical Patterns
### Skill Invocation
Skills are invoked through an explicit `$skill` UserPromptSubmit, not by reading files directly. The hook resolves the nearest project-local protected skill first, then the global protected skill, and injects the full `SKILL.md` as trusted `additionalContext`:
```
$prometheus // explicit UserPromptSubmit; full protected SKILL.md arrives as additionalContext
Read("skills/prometheus/SKILL.md") // Wrong
```
On Codex, the marker is only an invocation audit/integrity record; it is not authorization. PreToolUse always denies a literal direct read of a model-disabled skill body, whether or not a marker exists (or has been forged).
### Subagent Selection
| Need | Agent |
|------|-------|
| Architecture/debugging analysis | oracle |
| Codebase search | explore |
| External documentation | librarian |
| Code implementation | sisyphus-junior |
| Pre-planning analysis | metis |
| Plan review | momus |
| Quality Assurance | qa skill |
### sync.yaml Paths Are Machine-Specific
`sync.yaml:path` contains absolute paths to target projects. These are local to each developer's machine — do not commit personal paths in PRs.
### Cache-Safe Context Injection
Hook and skill authors who emit injected context (SessionStart stdout, keyword-detector payloads, skill `` !`command` `` macro output) MUST follow these constraints. The goal: bytes that land in the PREFIX segment of the conversation must be session-invariant so the KV cache is not evicted on every new session. A single varying byte anywhere in the prefix evicts the entire downstream cache.
- **No per-request volatile values in PREFIX-position injected context.** Timestamps, PIDs, ephemeral counters, or any value that changes between requests must not appear in context injected into the conversation prefix.
- **Sort collections (deterministic ordering) before emitting.** Any list, set, or map serialized into injected context must be sorted before output. Insertion-order or filesystem-order enumerations are non-deterministic across sessions and defeat caching.
- **Session-varying values: coarsen OR use a static state-file pointer.** If a value legitimately varies by session but must appear in injected context, either coarsen it to a stable category (e.g., a count bucket rather than an exact count), or emit a static shell read command — `cat "$OMT_DIR/<state>-$OMT_SESSION_ID.json"` — with a run-now imperative. The pointer string itself is static; the actual read happens in TAIL position, not PREFIX.
- **SessionStart stdout = static; route dynamic/volatile data to stderr or on-demand reads.** The SessionStart hook's stdout is injected directly into the conversation prefix. Keep it fully static. Emit diagnostic or session-varying information to stderr (logged, not injected) or defer it to an on-demand read instruction executed later in the conversation body.
- **Skill-body `` !`command` `` macro output must be deterministic + session-invariant.** Command substitutions embedded in SKILL.md via the `` !`...` `` macro are evaluated at skill-load time and injected into the prefix. Their output must be bit-for-bit identical across sessions; any path, timestamp, or environment-specific value disqualifies a command from macro use.
## Language Conventions
- **Commit messages**: Korean (한국어) with 명사형 종결
- **DisplayNames in tests**: Korean
- **Method names in tests**: English with backticks
- **Council prompts**: English (for cross-model consistency)
## Dependency Management
OMT minimizes external dependencies to stay auditable and portable across the runtimes it targets (bun, node). Reach for the simplest option first.
### Dependency Ladder
1. **Builtin first** — use a bun/node builtin with no added dependency. See Tier-0 allowlist below.
2. **Declared package** — add the package to `package.json` (`dependencies` or `devDependencies`) and write a plain bare `import 'pkg'` in source. `make sync` handles the rest at sync-time (see below).
There is no installable npm package — OMT is not published to a registry.
### Tier-0 Builtin Allowlist
These are pre-approved — use them without reaching for a dep:
- `Bun.YAML` — YAML parse/serialize (requires bun ≥ 1.2.21)
- `fetch` — HTTP requests
- `crypto.randomUUID` — UUID generation
- `util.parseArgs` — CLI argument parsing
- `fs.glob` — file globbing
- `module.builtinModules` — querying available builtins
### Sync-Time Auto-Vendoring
No vendor artifacts are committed to this repository. Instead, `make sync` bundles each declared bare dependency at sync-time:
1. For every bare `import 'pkg'` found in a deployed script, the sync tool checks that `pkg` is declared in the root `package.json`.
2. At sync-time, it runs `bun build --target=node` to produce `lib/vendor/<pkg>.js` inside each deploy target and rewrites that copy's import to a relative path. OMT source files are never mutated.
3. Integrity rests on the committed `bun.lock` (version pins + sha512 checksums) enforced by `bun install --frozen-lockfile` — no separate byte-drift manifest is needed.
### Guards
Enforcement is wired into the make targets — do not reimplement inline:
- **Bare-import guard** (`make validate`): scans the deployed surface — `lib/` and the component dirs (`hooks/`, `skills/`, `scripts/`, `agents/`, `commands/`, `rules/`), plus `projects/*/` equivalents — and rejects a bare `import 'pkg'` for a package NOT declared in `package.json`. `tools/` is exempt (npm imports are legal there, where `node_modules` exists), and `*.test.ts` / `*.d.ts` files are skipped. A declared package passes; a sub-path import of a declared package is still rejected.
- **`bun.lock` integrity**: version pins and sha512 checksums are committed; `bun install --frozen-lockfile` enforces them.
### Cross-Runtime Caveat
Scripts reachable by codex or gemini must restrict themselves to cross-runtime builtins (i.e., Node.js built-in modules that also run under bun). Packages bundled at sync-time use `--target=node` so they execute under both runtimes.
### Non-Goals
- No committed vendor bundles in source — bundles are generated at sync-time into deploy targets only.
- No committed `node_modules` — builtins first, declared packages second.
- No install-at-runtime in production — `bun install --frozen-lockfile` runs at build/CI time, not at agent invocation time.