{"owner":"can1357","repo":"oh-my-pi","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md"],"files":{"AGENTS.md":"# Development Rules\n\n## Default Context\n\nThis repo contains multiple packages, but **`packages/coding-agent/`** is the primary focus. Unless otherwise specified, assume work refers to this package.\n\n**Terminology**: When the user says \"agent\" or asks \"why is agent doing X\", they mean the **coding-agent package implementation**, not you (the assistant). The coding-agent is a CLI tool — questions about its behavior refer to code in `packages/coding-agent/`, not your current session.\n\n### Package Structure\n\n| Package                 | Description                                                                             |\n| ----------------------- | --------------------------------------------------------------------------------------- |\n| `packages/ai`           | Multi-provider LLM client with streaming support                                        |\n| `packages/catalog`      | Model catalog: bundled models.json, provider descriptors, model identity/classification |\n| `packages/agent`        | Agent runtime with tool calling and state management                                    |\n| `packages/coding-agent` | Main CLI application (primary focus)                                                    |\n| `packages/tui`          | Terminal UI library with differential rendering                                         |\n| `packages/natives`      | Bindings for native text/image/grep operations                                          |\n| `packages/stats`        | Local observability dashboard (`omp stats`)                                             |\n| `packages/omptype`      | ArkType-compatible schema validation with a lazy JIT runtime                            |\n| `packages/utils`        | Shared utilities (logger, streams, temp files)                                          |\n| `crates/pi-natives`     | Rust crate for performance-critical text/grep ops                                       |\n\n**Catalog import convention**: code in this repo imports catalog _values_ (bundled models, model-thinking helpers, identity, descriptors, model manager/cache) from `@oh-my-pi/pi-catalog/<module>` — never via `@oh-my-pi/pi-ai`. The pi-ai barrel re-exports only the model/effort _types_ its own signatures use (`Model`, `Api`, `ThinkingConfig`, `Effort`, …); type-only imports of those from `@oh-my-pi/pi-ai` are fine.\n\n## GitHub\n\nUnless user tells you exactly what to write:\n\n- **Never comment on GitHub** (issues, PRs, discussions).\n- **Never create issues on GitHub**.\n\n## Code Quality\n\n- No `any` unless absolutely necessary.\n- **NEVER use `ReturnType<>`** — use the actual type name.\n- **NEVER use inline imports** — no `await import()`, no `import(\"pkg\").Type` in type positions, no dynamic type imports. Always top-level.\n- Check `node_modules` for external API types instead of guessing.\n- **Barrel exports**: prefer `export * from \"./module\"` over named re-exports, including `export type { ... } from`. In pure `index.ts` barrels, use star re-exports even for single-specifier cases. If stars create ambiguity, remove the redundant export path; do not keep duplicates.\n- **Class privacy**: use ES `#private` fields; leave externally accessible members bare. **No `private`/`protected`/`public` keyword on fields or methods**, except on **constructor parameter properties** where TypeScript requires it (e.g. `constructor(private readonly session: ToolSession)`).\n- **Promises**: use `Promise.withResolvers()` instead of `new Promise((resolve, reject) => ...)`.\n- **Prompts**: never build prompts in code (no inline strings, template literals, or concatenation). Prompts live in static `.md` files; use Handlebars for dynamic content. Import them via `import content from \"./prompt.md\" with { type: \"text\" }` — not `readFile`.\n- **Worker scripts**: workers re-enter the CLI entrypoint; never spawn separate worker entry modules. `cli.ts` declares itself as the worker host at startup (`declareWorkerHostEntry()` from `@oh-my-pi/pi-utils/env`) and dispatches hidden argv selectors (`__omp_worker_stats_sync`, `__omp_worker_tab`, `__omp_worker_js_eval`, `__omp_worker_tiny_inference`) before loading the command registry. Spawn sites use:\n  ```ts\n  import { workerHostEntry } from \"@oh-my-pi/pi-utils\";\n  const hostEntry = workerHostEntry();\n  const worker = hostEntry\n  \t? new Worker(hostEntry, { type: \"module\", argv: [\"__omp_worker_<name>\"] })\n  \t: new Worker(new URL(\"./<worker>.ts\", import.meta.url).href, { type: \"module\" });\n  ```\n  When the process was started from the omp CLI — source `cli.ts`, npm-bundle `dist/cli.js`, or compiled binary — `workerHostEntry()` is `Bun.main` and the worker re-enters the single entry module, so no per-worker `--compile` entrypoints or bundle entries exist. Outside a CLI host (`bun test`, SDK embedding, standalone `omp-stats`) it returns `null` and the direct-module fallback loads the worker source. New worker kinds MUST add their selector to the dispatch table in `cli.ts` and keep the fallback branch.\n  History: `with { type: \"file\" }` only copied the entry as a raw asset (workers crashed silently in compiled binaries — issues #1011, #1027), and the later literal-path + extra-entrypoint pattern required keeping spawn literals and two build scripts in sync (issue #1150). The smoke probe below is the live validation of this contract.\n  Validate any new worker with the dedicated smoke probe: `omp --smoke-test` spawns the stats sync worker and the tiny-model subprocess, pings them, and exits — it's wired into `ci:test:smoke` and `scripts/install-tests/run-ci.sh` so binary, source-link, and tarball installs all exercise it. Add a sibling smoke if the new worker is on a different module graph.\n\n## Central Utilities\n\nBefore writing a helper, check whether one already exists — `packages/coding-agent/src/utils/`, `@oh-my-pi/pi-utils`, `@oh-my-pi/pi-tui`, and the domain modules next to your callsite. This applies to **everything**: VCS wrappers, formatting/truncation/path-display helpers, image handling, clipboard, streams, temp files, caching. The central versions carry hardening a fresh copy always loses (timeouts, output caps, non-interactive env, lock avoidance, caching, TUI sanitization).\n\n- Search first: `grep` for the operation before implementing it. Two implementations of the same thing is a bug even when both work.\n- Examples of the pattern: `src/utils/git.ts` and `src/utils/jj.ts` are the only sanctioned way to run git/jj (`import * as git from \"../utils/git\"` — never hand-spawn via `$`/`Bun.spawn`); rendering goes through the helpers in TUI Sanitization below (`replaceTabs`, `truncateToWidth`, `shortenPath`, `PREVIEW_LIMITS`) rather than ad-hoc string math.\n- Missing capability? Extend the central helper (new option, new sub-function on the namespace) and call it — don't fork its logic locally.\n\n## Bun Over Node\n\nUse Bun APIs where they provide a cleaner alternative; fall back to `node:*` only for what Bun doesn't cover. **Never spawn shell commands for operations with proper APIs** (e.g., don't `Bun.spawnSync([\"mkdir\", \"-p\", dir])` — use `mkdirSync`).\n\n### Quick reference\n\n| Operation       | Use                                       | Not                                |\n| --------------- | ----------------------------------------- | ---------------------------------- |\n| File read/write | `Bun.file()`, `Bun.write()`               | `readFileSync`, `writeFileSync`    |\n| Spawn process   | `` $`cmd` ``, `Bun.spawn()`               | `child_process`                    |\n| Sleep           | `Bun.sleep(ms)`                           | `setTimeout` promise               |\n| Binary lookup   | `$which(\"git\")` from `@oh-my-pi/pi-utils` | `spawnSync([\"which\", \"git\"])`      |\n| HTTP server     | `Bun.serve()`                             | `http.createServer()`              |\n| SQLite          | `bun:sqlite`                              | `better-sqlite3`                   |\n| Hashing         | `Bun.hash()`, `Bun.password.*`, WebCrypto | `node:crypto`                      |\n| Path resolution | `import.meta.dir`, `import.meta.path`     | `fileURLToPath` dance              |\n| JSON5           | `Bun.JSON5.parse()` / `.stringify()`      | `json5` package                    |\n| JSONL           | `Bun.JSONL.parse()` / `.parseChunk()`     | `text.split(\"\\n\").map(JSON.parse)` |\n| String width    | `Bun.stringWidth()`                       | `get-east-asian-width`, custom     |\n| Text wrapping   | `Bun.wrapAnsi()`                          | custom ANSI-aware wrappers         |\n\n### Process execution\n\nPrefer Bun Shell (`` $`cmd` ``) for simple commands:\n\n```typescript\nimport { $ } from \"bun\";\n\nconst result = await $`git status`.cwd(dir).quiet().nothrow();\nif (result.exitCode === 0) {\n\tconst text = result.text();\n}\n\n$`do-stuff ${tmpFile}`.quiet().nothrow(); // fire and forget\n```\n\nMethods: `.quiet()`, `.nothrow()`, `.text()`, `.cwd(path)`.\n\nUse `Bun.spawn`/`Bun.spawnSync` only for: long-running processes (LSP, kernels), streaming stdin/stdout/stderr (SSE, JSON-RPC), or process control (signals, kill, complex lifecycle).\n\nWhen using `pipe` mode, cast the stream:\n\n```typescript\nconst child = Bun.spawn([\"cmd\"], { stdout: \"pipe\", stderr: \"pipe\" });\nconst reader = (child.stdout as ReadableStream<Uint8Array>).getReader();\n```\n\n### Node module imports\n\nAlways use **namespace imports** for `node:fs`, `node:path`, `node:os`:\n\n```typescript\nimport * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\n```\n\n- Async-only file → `node:fs/promises`.\n- Needs both sync and async → `node:fs`, then `fs.promises.xxx` for async.\n\n### File I/O\n\nPrefer Bun:\n\n```typescript\nconst text = await Bun.file(path).text();\nconst data = await Bun.file(path).json();\nawait Bun.write(path, data); // auto-creates parent dirs\n```\n\nUse `node:fs/promises` for directory ops (`fs.mkdir`, `fs.rm`, `fs.readdir`) — Bun has no native directory APIs. Avoid sync APIs in async flows; use sync only when forced by a synchronous interface.\n\n**Anti-patterns:**\n\n- `existsSync`/`readFileSync`/`writeFileSync` in async code → `Bun.file()` APIs.\n- `mkdir(dirname(path), …)` before `Bun.write(path, …)` → redundant; `Bun.write` handles it.\n- `if (await file.exists()) { await file.json() }` → two syscalls plus race. Use try-catch with `isEnoent`:\n  ```typescript\n  import { isEnoent } from \"@oh-my-pi/pi-utils\";\n  try {\n  \treturn await Bun.file(path).json();\n  } catch (err) {\n  \tif (isEnoent(err)) return null;\n  \tthrow err;\n  }\n  ```\n- Multiple `Bun.file(path)` handles for the same path (including across `checkX`/`loadX` helpers).\n- `Buffer.from(await Bun.file(x).arrayBuffer())` → `await fs.readFile(path)`.\n- Existence check + try-catch around the same read → drop the existence check.\n\n### Streams\n\nPrefer centralized helpers:\n\n```typescript\nimport { readStream, readLines } from \"./utils/stream\";\nconst text = await readStream(child.stdout);\nfor await (const line of readLines(stream)) {\n\t/* ... */\n}\n```\n\nManual reader loops only when the protocol requires it (SSE, streaming JSON-RPC).\n\n### Misc\n\n- **Sleep**: `await Bun.sleep(ms)`, never `new Promise(r => setTimeout(r, ms))`.\n- **Password hashing**: `Bun.password.hash(pw, \"bcrypt\")` / `Bun.password.verify(pw, hash)`.\n- **String width**: `Bun.stringWidth(text, { countAnsiEscapeCodes?: false })`.\n- **Wrapping**: `Bun.wrapAnsi(text, width, { wordWrap, hard, trim })`.\n\n## Generated Files\n\n**NEVER edit `packages/catalog/src/models.json` directly.** It is generated from upstream sources (stencil.so, provider catalog discovery, OpenCode docs) by `packages/catalog/scripts/generate-models.ts` and the descriptors/resolvers in `packages/catalog/src/provider-models/`. Hand-edits get overwritten on the next regen.\n\nTo change an entry, fix the source:\n\n- **Resolution rules / per-id overrides** → relevant resolver in `packages/catalog/src/provider-models/openai-compat.ts` (e.g. `createOpenCodeApiResolution`'s id-override map).\n- **Provider catalog entries** (default model, discovery factory/flags) → the `CATALOG_PROVIDERS` table in `packages/catalog/src/provider-models/descriptors.ts`.\n- **Generator-level fixups** (premium multipliers, codex pricing fallback, fallback models, post-processing) → `packages/catalog/scripts/generate-models.ts`.\n- **Thinking metadata / generated policies** → `packages/catalog/src/model-thinking.ts` (`applyGeneratedModelPolicies`); model-id classification (family/version parsing) lives in `packages/catalog/src/identity/classify.ts`.\n\nRegenerate with `bun run gen:models` and commit `models.json` alongside the source change. Add a regression test against the **resolver/descriptor**, not the bundled JSON, so it survives upstream metadata shifts.\n\n## Logging and CLI Output\n\nCode that may run while the TUI, RPC, SDK, workers, or background runtimes are active MUST NOT use `console.log`/`error`/`warn`; it corrupts rendering or protocols. Use the centralized logger:\n\n```typescript\nimport { logger } from \"@oh-my-pi/pi-utils\";\n\nlogger.error(\"MCP request failed\", { url, method });\nlogger.warn(\"Theme file invalid, using fallback\", { path });\nlogger.debug(\"LSP fallback triggered\", { reason });\n```\n\nLogs go to `~/.omp/logs/omp.YYYY-MM-DD.log` with automatic rotation. Standalone CLI commands that exit without entering the TUI MAY use `console.*` or process streams for intentional user-facing output. Keep structured stdout clean. This exception is semantic, not filename-based; shared code must use `logger` or an explicit output sink.\n\n## TUI Sanitization\n\nAll text displayed in tool renderers must be sanitized. Raw content (file contents, error messages, tool output) breaks terminal rendering: tabs → visual holes, long lines → overflow, paths → leak home directory.\n\n**Rules:**\n\n- **Tabs → spaces** via `replaceTabs()` (from `@oh-my-pi/pi-tui` or `../tools/render-utils`).\n- **Truncate** lines with `truncateToWidth()` / `ui.truncate()`. Use `TRUNCATE_LENGTHS` constants.\n- **Shorten paths** with `shortenPath()` (replaces home with `~`).\n- **Preview limits** from `PREVIEW_LIMITS`. No ad-hoc numbers.\n\n**Apply to every render path**, not just the happy one:\n\n- Success output (file previews, command output, search results).\n- **Error messages** — these often embed file content (e.g., patch failure messages include unmatched lines). If a message contains file content, it needs `replaceTabs()`.\n- Diff content (added and removed).\n- Streaming previews.\n\n### Streaming tool previews\n\nTool-call previews can have **multiple render paths**. If you add preview-only fields or depend on partially streamed args, update every path — not only the final renderer. Streamed argument buffers decode into display args via `decodeStreamedToolArgs` / `ToolArgsRevealController` (`modes/controllers/tool-args-reveal.ts`); both the live event path and transcript rebuilds must go through them — never spread provider-parsed `arguments` next to a raw `__partialJson` (parsed args lag the stream by a throttled parse window).\n\nFor the bash tool specifically:\n\n- The pending preview may need raw `partialJson`, not just parsed `arguments`. Parsed args lag until a JSON object closes, which makes inline env assignments appear only at the end.\n- Preserve preview-only fields (e.g. `__partialJson`) through `event-controller.ts`, transcript rebuilds in `ui-helpers.ts`, and merged call/result rendering in `tool-execution.ts`. Missing one path causes inconsistent previews.\n- `ToolExecutionComponent.#buildRenderContext()` for bash must work even before a result exists — the renderer uses call args plus render context to show the command preview while streaming.\n- Verify both live streaming and rebuilt transcript paths after any bash preview change. A fix in one path does not fix the other.\n\n## Commands\n\n- NEVER commit unless asked.\n- Never use `tsc`/`npx tsc` — always `bun check`.\n- Merge commits (maintainer merges of PRs) follow: `Merge PR #<number>: <conventional PR subject> (@<author>)` — e.g. `Merge PR #6386: feat(catalog): add native Meta Model API provider (@eggpeat)`.\n\n## Testing Guidance\n\nTest the contract the system exposes — not the easiest internal detail to assert.\n\n- Every new test must defend one **concrete, externally observable contract**: behavior, output shape, state transition, error mapping, or a regression-prone parsing boundary. If you cannot name the contract, do not add the test.\n\n### Good vs. bad test filter\n\n- **Name the failure mode.** Every test MUST state what a consumer observes if it regresses. Cannot name one? NEVER add it.\n- **Good: transformation.** One fixture MAY prove parse/render/normalize/encode/resolve behavior when output is computed, not echoed.\n- **Good: branch or boundary.** Distinct inputs, empty values, malformed input, version/provider routing, and state transitions MUST prove distinct outcomes.\n- **Good: external contract.** Exact bytes/shape MAY be asserted when a provider, parser, protocol, or persisted consumer reads them.\n- **Good: precedence or negative contract.** Keep explicit `false`/override-wins assertions and required absence only when they prevent a documented leak, downgrade, 400, or incompatible wire field.\n- **Good: regression.** A repro MUST trigger the prior real failure path and assert the corrected observable result.\n- **Bad: static echo.** NEVER test a constructor/builder merely copied a fixture or baked constant into an in-memory config/metadata field.\n- **Bad: success passthrough.** NEVER assert `fn(x) === x` when `x` was already supplied/declared valid; assert a transform, rejection, or downstream effect instead.\n- **Bad: wording/defaults.** NEVER assert prompt/UI boilerplate, a default literal, object existence, non-empty output, or length growth without a consumer contract.\n- **Bad: duplicate rows.** Parameterized/loop rows MUST each cover a distinct branch, provider/model path, or consumer contract; delete same-path duplicates.\n- **Metadata exception.** Exact metadata, identity, ordering, or `undefined` MAY remain only when a downstream consumer depends on it and the test establishes branch, precedence, negative-contract, wire, or regression evidence.\n- **Termination exception.** For cyclic/large inputs, assert a bounded output, surfaced error, or state change; bare `not.toThrow()` is insufficient.\n- No placeholder tests, tautologies, or \"the code ran\" assertions (`expect(true).toBe(true)`, bare `not.toThrow()`, non-empty string checks, length-grew checks, \"prompt exists\" checks without semantic assertion).\n- Prefer contract-level tests over implementation details. Avoid asserting internal helper wiring, field assignment, singleton identity, incidental ordering, prompt boilerplate, or passthrough option forwarding unless another component depends on that exact detail.\n- Don't duplicate coverage across abstraction levels. If an integration test already proves the behavior, drop the narrower unit test that restates it through mocks.\n- Tests **must be full-suite safe**, not just file-local safe. No long-lived file-wide mutations of `Bun.*`, `process.platform`, `process.env`, or `Bun.env` when a narrower seam exists. Prefer per-test `vi.spyOn(...)` with `vi.restoreAllMocks()` in `afterEach`. A test that passes alone but poisons later files is broken.\n- **Never use `mock.module()`**. Bun's `mock.module()` mutates the global module registry and leaks across files ([oven-sh/bun#12823](https://github.com/oven-sh/bun/issues/12823)). Use `spyOn` on the imported module object instead. For pass deps, import the pass and spy on `.run`. For package deps, namespace-import and spy on the exported function.\n- For lifecycle/stateful code, prefer one test per invariant or transition over several tiny tests asserting one field each from the same transition.\n- For error handling, trigger the real failure path and assert the surfaced contract — don't instantiate error classes directly or inspect internal metadata.\n- Smoke tests are acceptable only when they catch a failure mode narrower tests would miss. \"Package boots\" or \"command starts\" alone is not enough.\n- Assert exact strings, ordering, and formatting only when downstream code parses or depends on the exact bytes. Otherwise assert semantic content.\n- Compile-time guarantees → type checks/type tests, not runtime placeholders.\n- **Never source-grep.** A test that reads an implementation file (`.ts`/`.rs`/build script) and asserts on its _text_ — `expect(src).toContain(\"someCall()\")`, `.toMatch(/import .../)`, `.not.toContain(\"oldName\")`, or \"comment must say X\" — is banned. It tests how code _looks_, not what it _does_: it breaks on harmless refactors (comment reflow, rename, import reorder) and passes while the behavior is broken. Assert the observable contract instead (run the code, check output/state/error), use the runtime smoke probe for wiring you cannot exercise in-process, and enforce structural invariants (no value-import of X, no self-import) with a type test or a lint/biome rule — never a string scan of the source. (Reading a file your code _wrote_ — apply-patch result, generated bundle, temp fixture — and asserting on that output is fine; that is behavior, not a source grep.)\n- Don't add tests for tiny low-risk changes unless they protect a real contract or fix a regression-prone edge case.\n- Prefer focused package-local verification for the changed area.\n\n## Changelog\n\nLocation: `packages/*/CHANGELOG.md` (per package).\n\n**Format** — sections under `## [Unreleased]`:\n\n- `### Breaking Changes` (first if present)\n- `### Added`\n- `### Changed`\n- `### Fixed`\n- `### Removed`\n\n**Rules:**\n\n- New entries always go under `## [Unreleased]`.\n- Never modify already-released sections (e.g., `## [0.12.2]`) — they are immutable.\n- Don't flag changelog section order or formatting in reviews or PRs — `bun run release` runs `fix-changelogs` which normalizes everything automatically.\n\n**Attribution:**\n\n- Internal (from issues): `Fixed foo bar ([#123](https://github.com/can1357/oh-my-pi/issues/123))`.\n- External contributions: `Added feature X ([#456](https://github.com/can1357/oh-my-pi/pull/456) by [@username](https://github.com/username))`.\n\n## Releasing\n\n1. Ensure all changes since last release are in each affected package's `[Unreleased]` section.\n2. Run `bun run release`.\n\nThe script handles version bump, CHANGELOG finalization, commit, tag, publish, and adding new `[Unreleased]` sections.\n"}}