CLAUDE.md · git:20261017.28c31d0 · 2026-10-17 · sha256 89e6340874803bbf

CLAUDE.md git:20261017.28c31d0A

Immutable. This exact content never changes and is served at /api/v1/blob/89e6340874803bbf.

# 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; whole run at background priority via `taskpolicy -b`)
```

`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 installs it with the Puppeteer bundled
inside `mmdc` (`npx puppeteer` would install the Chrome version of npm's latest
Puppeteer, which can differ from the one `mmdc`'s Puppeteer looks for).

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
"$(dirname "$(realpath "$(command -v mmdc)")")/../node_modules/.bin/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 → user scenario → record → verdict → complete chain; QA reads the plan/issue/spec/PR itself and writes ACs as user-observable outcomes in the report language (the CLI refuses a camelCase/snake_case code name in an AC, story goal, client-impact reason or risk-N/A reason) (a state file is never the source of requirements); QA checks the product in one target environment, `local` unless the user names another (a rollout plan never picks it), and sorts every source or caller item first: product behavior becomes an AC proven there, release/operations work (deploying, post-deploy observation, production records, follow-up PRs) goes to `## Out of QA Scope` (never an AC or `blocked`), and provenance (PR, wiki) is input never queried on GitHub; an item it cannot place is asked of the user before `set-acceptance`; actors carry `client_impact` none|contract|render + reason (one actor per client that reads what changed; "screen unchanged" means `contract`, proven at the client's real request; `render` needs a screen driver and device profiles from `qa-device-profiles.ts get|set` — per-project `~/.qa-cases/<projectKey>/device-profiles.yaml`, unconfigured → show the built-in defaults and ask the user once, "don't know" → `set --defaults`, then `remove`/`upsert` fit the list to the project's platforms, never guess sizes, each render story needs ≥1 screen-driven scenario per profile with before/after screenshots (the same screenshot is refused across profiles) (a scenario proven by `--evidence-surface test` carries no profile), and its `review-evidence` needs a `kind: "layout"` claim with `checked` clipping/overlap/horizontal-scroll/text-wrap); `author-scenario` writes user scenarios (title, preconditions, steps, expected, why-needed, priority, risks[1..6], profile) with ≥1 H per story, and each of the six risk tags is covered by a scenario or declared via `declare-risk-na --axis --reason` once per cycle; `amend-scenario --reason` corrects a scenario that already has a result (result reset so it is re-run and re-recorded, prior record kept in `scenario_amendments`, stored verdict removed, no cycle or FIX attempt consumed, and `author-scenario` refuses such a scenario); new stories require goal + non-empty Given/When/Then arrays + zero-based acceptance-criteria links, while old records remain readable but not execution-ready and contract changes are rejected once current-cycle evidence exists; runtime gates block unrecorded drivers and Stop; `complete` (and ultragoal `request-complete`) refuses while a recorded background resource is not yet stopped via `release-resource`; simulators/emulators come only from `acquire-device`, which creates a session-owned device (iOS `omt-<session>-<n>`, Android read-only instance tagged `qemu.omt.session=<session>` in host argv, plus any flags the project's launcher documents via `--emulator-args '<JSON array>'`) so concurrent sessions never stop each other's devices, and servers are recorded with `record-resource` (a resource set on a device takes `--device <serial>` and is released with that device; a boot failure prints the emulator's last output); a human-only decision the AI cannot make (e.g. a question about the requirement itself) parks Stop via `await-user` without spinning the no-progress counter (cleared by the next progress write); the report shows one banner above the overview counting the `blocked` scenarios and stating the verdict was made without verifying them; the user-only `force-complete --reason` ends a cycle the gates will not close (recorded as `forced_complete`, Stop-allowed, cleared by the next `start`) and still releases recorded resources, warning about any whose stop command fails; the local environment is the AI's to run under a standing user authorization — it starts, stops, resets, recreates and reseeds any local stack (a pre-existing, worktree or user-named one included) without asking or backing up, gets a fresh state from a new isolated stack or, when the repo cannot run two, a direct reset, and asks only before changing a physical device's data or a non-local environment; verification follows an economy principle — each changed story/scenario is proven by the cheapest means that proves it (server-only → `curl`; UI → one browser/device; a device only for screen-only claims, released at once), and an automated test run that asserts the scenario counts as scenario evidence via `--evidence-surface test`; a human-only step (pairing code, OTP) goes through `await-user`, never skipped or `blocked`; scenarios end as `pass`/`fail`, `blocked` (attempted; `--obstacle-kind hardware|third-party|person` only — absent hardware, an off-network third party, or a person — a `person` blocker records the user's next message after `await-user`, which `qa-user-reply.sh` stores verbatim, never agent-supplied text; a local account, seed, onboarding, permission or app/device state is setup work the CLI refuses as a cause; obstacle + attempts + deepest reachable point + attempt log, all audit-only; the reader card shows a fixed "검증 불가" line, the required `observed` and any recorded screenshots), or stay `unrecorded`; the flaky-rerun run check resets, then re-drives the H scenarios at the boundary (a build-tool cache hit proves nothing; no `--force`; tests run via their own runner); REQUEST_CHANGES needs a recorded product failure (a failed scenario counts only once its `review-evidence` carries a supported `kind: "cause"` claim checking `product-path` and `base-commit`; a failure may be reviewed without one, and an H fail whose cause stays unproven caps the verdict at COMMENT), APPROVE needs every scenario `pass` (any blocked scenario, or a flaky-rerun recorded `blocked` because the environment kept it from running, allows COMMENT at most), and an unrecorded scenario with no failure leaves no verdict; an unproven requirement is `unverified` (requires a `blocked` scenario), never green; visual claim reviews bind observations to evidence hashes; completion requires a fresh inspected HTML receipt; STATE HTML (Korean reader layer) leads with a verdict summary line and a context-free PO presentation layer (its feature overview describes the change, never the QA result — the render refuses a verdict, 검증 불가/미검증 or a confidence score there, and a code name in any reader prose) — AC board "요구사항(AC) 충족 현황", `requirementMapping` via `scenarioRefs` [{story, scenario}], risks declared N/A folded under one toggle, per-actor client-impact line and per-profile coverage, diagram on a fixed light panel in dark mode, 5-column audit table — anchored to recorded actors/stories/ACs; `review-evidence --story --scenario` binds observations to evidence hashes; reusable-case storage uses `qa-cases.ts`, with only its pointer/mode in the fixed external `~/.qa-cases/<projectKey>/manifest.yaml` and the store on by default at `~/.qa-cases/<projectKey>/cases` (disabled only on the user's request), and no implicit project files; the feature map (`~/.feature-maps/<project-key>/store`) is on by default too; a case is one scenario found by feature (`feature_refs` must be feature-map ids; `list --feature`), and every H scenario passed by driving its boundary is recorded while driven, saved, replayed as the flaky-rerun, and linked with `record-case --case ID` (only after a clean replay this cycle) or `--none "<reason>"` — `complete` refuses while one has neither; native runner formats remain native; `qa-replay.ts --help` exposes replay that checks the active complete chain, current story/scenario/cycle, and hashes, and that the case's `feature_refs` overlap the story's recorded provenance features (`qa-replay.ts --scenario`), creates a non-PASS receipt binding `scenario_id`, and may be bound with `qa-state.ts record-scenario --case-run` only alongside actual boundary evidence (see `skills/qa/reusable-cases.md`); a report finding on renderer-owned layout is noted as a known limitation and renderer follow-up, never a scope-expansion ask or `await-user` (QA does not edit `qa-report.ts` in a cycle); a scenario counts as failed for every AC it backs, so an AC judged on its own gets its own scenario |
| explain-diff | Diff explanation ending in a comprehension quiz | Completion is the reader passing, not the document; evidence → background → goal → architecture → capability → intuition → commits → code → render → quiz, goal states purpose+core before mechanism, capability step = top-level `## 기능 단위` chapters (one use-case each, versioned; domain/persistence fns are collaborators not capabilities), Intuition examples continue through code and changed-input prediction questions, system level = cross-process boundaries, template-owned visuals (style invention rejected), no quiz exemption |
| read-db | Read-only PostgreSQL lookups | Every query goes through `scripts/read-db.ts <service> "<sql>"`: `-ro`-suffixed `pg_service.conf` service only, read-only session, one SELECT-family statement per call, 15 s timeout, 200-row cap; the connection string never reaches argv — password comes from `~/.pgpass`; a local stack's database on this machine is queried with `psql` directly |
| 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; starts immediately with the inlined core loop, delegates to `help <topic>` only for specialized tasks |
| 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
- **compact-instructions.sh**: PreCompact hook — plain stdout is appended to Claude Code's native compaction summary instructions (a JSON `hookSpecificOutput` is ignored on this event); wording was tuned by blind-judged compaction replays. Claude-only, no Codex twin, since Codex's remote compaction path ignores `compact_prompt`.
- **session-ledger/**: PreCompact + SessionStart hook on both Claude and Codex (Codex via `--platform codex`). PreCompact extracts the transcript deterministically (`extract.ts`), runs a headless `codex exec -m gpt-6.1-sol` (effort low, fast tier) against `schema.json` with `prompt.md`, validates quotes/identifiers/ids and retries with the violations as feedback (`ledger.ts`, 3 attempts, 25-minute deadline, deterministic fallback), and writes `~/.omt/session-ledger/<sid>.md` + `.meta.json`; it prints nothing, because Claude appends PreCompact stdout to the summary instructions. SessionStart injects only for `source=compact` (a head of the ledger plus a read-the-whole-file order). The nested summarizer session runs with `OMT_LEDGER_WORKER=1` so the hook does not recurse; subagent payloads (`agent_id`) are skipped.
- **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 AI-runnable `resume-pursuit --reason` recovery (recorded in `budget_extensions`). Two named gates park the pursuit Stop-allowed WITHOUT counting no-progress: `renewal-required` (`claim-review-dispatch` sets it on review-budget exhaustion; exits via `approve-review-dispatch-renewal` or `force-complete`) and the `awaiting_user` flag (`await-user` sets it for a human-only decision — wrong plan/requirement or unsafe boundary — cleared by the next steering/progress write). An active qa cycle blocks Stop until a satisfied verdict + reviewed report, except that an `awaiting_user` flag (`await-user`, for a human-only decision about the requirement) parks Stop WITHOUT counting no-progress, cleared by the next progress write. 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 lacks a fresh Stage A presentation. The plan counts as written when `steps.plan.done` is set OR the plan file has a `## TODOs` section with TODO lines (`planFileHasTodos` in `lib/state-core.ts`); the token counts only alone on its own line outside fenced code; freshness uses the shared predicate `stageAPresentationStatus` in `lib/state-core.ts` (same rule as the state CLI's S6+ gate); refusals use their own counter capped at `MAX_BLOCK_COUNT` (5), after which teardown proceeds; pre-plan aborts still tear down, and a recorded `set --abort --reason` (user abandoned the work) lets the token tear down without the presentation gate
- **pre-tool-enforcer.sh** / **codex-write-guard.sh**: PreToolUse gates — TaskOutput blocking, `git grep` without `-I` denial (binary files otherwise get scanned, several GB per search; shared recognition in `hooks/lib/git-grep-patterns.sh`), code-review artifact identity guard, and user-only state command guard (`force-complete` on `ultragoal-state.ts` and `qa-state.ts` only — it bypasses every completion gate, so the AI's Bash path is denied and the deny message tells the AI to show the command, run `await-user`, and end the turn; `approve-review-dispatch-renewal` / `dismiss-review-finding` / `resume-pursuit` are AI-runnable with a recorded reason that `request-complete` surfaces). A shared state-file write guard (`write-guard-core.sh`, Claude + Codex) blocks tool-level raw writes/deletes to the skill state files (`ultragoal-`/`goal-`/`prometheus-`/`deep-interview-active-`/`qa-`/`explain-diff-state-*.json`) so every state mutation routes through the validated CLI, while `bun *-state.ts` invocations stay allowed. 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 `approve-review-dispatch-renewal --reason` (AI-runnable, recorded in `budget_extensions`) renews the cap by 5. On cap exhaustion the claim parks the pursuit in `phase=renewal-required` (`active=false`, Stop-allowed, no no-progress counting); `approve-review-dispatch-renewal` both adds 5 and restores `pursuing`, and the user-only `force-complete` is the alternate exit.
- **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)
- **qa-user-reply.sh**: QA UserPromptSubmit hook shared by Claude and Codex — while `await-user` is pending, stores the user's prompt verbatim as `user_reply` and opens `user_answer_open` (under the CLI's state lock, tmp+mv), so a `person` blocker records what the user said; prints nothing, never blocks the prompt, and skips the write on a missing tool or a held lock
- **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 |
| Plan/design review (steelman antithesis, tradeoff tension analysis) | daedalus |
| Quality Assurance | qa skill |
| Presentation review (source fidelity, persona fit, concept introduction) | presentation-reviewer |

### 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.