This repo contains multiple packages, but packages/coding-agent/ is the primary focus unless stated otherwise.
Terminology: when the user says “agent” or asks why “agent” is doing X, they mean the coding-agent package implementation, not the current assistant session.
| Package | Description |
|---|---|
packages/ai |
Multi-provider LLM client with streaming support |
packages/agent |
Agent runtime with tool calling and state management |
packages/coding-agent |
Main CLI application (primary focus) |
packages/tui |
Terminal UI library with differential rendering |
packages/natives |
bindings for native text/image/grep operations |
packages/stats |
Local observability dashboard (spell stats) |
packages/utils |
Shared utilities (logger, streams, temp files) |
crates/pi-natives |
Rust crate for performance-critical text/grep ops |
Org-mode parsing, querying, and graph algorithms run natively via pi-org-engine in @oh-my-pi/pi-natives.
- Canonical tool:
executeOrgNAPI dispatch (parse,query,graph,computeWaves,nextWave,editSection,toMarkdown). - No Emacs dependency; the org tool runs without Emacs, socat, or other external processes.
- Writes stay in TypeScript;
org-writer.tshandles file mutations and the native engine stays read-only. - Plan execution uses
FluidOrchestratorwithOrgFluidBridgefor ITEM→DOING→DONE lifecycle.
- No
anytypes unless absolutely necessary. - Prefer
export * from "./module"over named re-export blocks, includingexport type { ... } from; in pure barrel files, use star re-exports even for single-specifier cases, and remove redundant exports if star re-exports create ambiguity. - Class encapsulation: ES
#private fields, bare for accessible. Noprivate/protected/publickeywords except constructor parameter properties. - No
ReturnType<>— use actual type names from source ornode_modulestype definitions. - Check
node_modulesfor external API types instead of guessing. - No inline imports: avoid
await import(...),import("pkg").Type, and dynamic imports for types; use top-level imports. - Never remove or downgrade code to fix type errors from outdated dependencies; upgrade the dependency instead.
- Ask before removing functionality or code that appears intentional.
- Never build prompts in code: no inline strings, template literals, or concatenation; prompts live in static
.mdfiles and dynamic content uses Handlebars. - Import static text files via Bun text imports (
import content from "./prompt.md" with { type: "text" }) instead ofreadFileSync. - Use
Promise.withResolvers()instead ofnew Promise((resolve, reject) => ...)for cleaner, typed resolvers.
Use Bun APIs when they are simpler; use Node APIs only where Bun lacks coverage or the task needs Node-specific behavior.
- Spawn shell commands only when no direct API exists.
- Process execution: use
$template literals for simple commands; useBun.spawnonly for long-running, streaming, or lifecycle-managed processes. - Sleep: use
await Bun.sleep(ms). - Node imports:
node:fs,node:path,node:os→import * asnamespace only. - File I/O:
Bun.file()for reads,Bun.write()for writes (auto-creates parent dirs), andnode:fs/promisesfor directory ops. - File I/O anti-patterns: no
exists()before read, no duplicate handles to the same path, noBuffer.from(await Bun.file(...).arrayBuffer()), and no redundant exists-check + try/catch. - Streams: use
readStream()andreadLines()helpers; manual reader loops only for SSE or streaming JSON-RPC. - JSON5/JSONL: use
Bun.JSON5andBun.JSONL; for streaming JSONL useparseChunk()/parse()without decoding first. - String width: use
Bun.stringWidth(). - Wrapping: use
Bun.wrapAnsi(). - Where Bun wins: file read/write, process spawn, sleep, binary lookup, HTTP server, SQLite, hashing, path resolution, JSON5, JSONL, string width, text wrapping.
- Anti-patterns:
Bun.spawnSyncfor simple commands,setTimeoutpromises for sleep, sync file APIs in async code, manualchild.stdout.getReader()loops for non-streaming commands,json5dependency, custom width logic, custom ANSI wrapping.
- Do not check
.exists()before reading; usetry/catchwithisEnoent(). - Do not create multiple handles to the same path.
- Do not use
Buffer.from(await Bun.file(x).arrayBuffer()); usereadFile. - Do not mix redundant existence checks with
try/catch.
- Subprocess streams: cast when using pipe mode.
- Password hashing: use Bun built-in bcrypt/argon2.
- Never use
console.log,console.error, orconsole.warnin the coding-agent package; console output corrupts the TUI rendering. - Use the centralized logger instead.
- Logs go to
~/.spell/logs/spell.YYYY-MM-DD.logwith automatic rotation.
All text displayed in tool renderers must be sanitized before output; raw file content, error messages, and tool output can break terminal rendering or leak home directories.
- Tabs → spaces: always pass displayed text through
replaceTabs()before rendering. - Line truncation: truncate displayed lines with
truncateToWidth()orui.truncate()and useTRUNCATE_LENGTHSfor consistency. - Path shortening: use
shortenPath()for user-visible file paths. - Content preview limits: use
PREVIEW_LIMITSconstants for collapsed/expanded line counts.
Apply sanitization to every TUI render path, including success output, error messages, diff content, and streaming previews; if a message includes file content, it needs replaceTabs().
- Preview-only fields must survive every preview/render path, including partial JSON, transcript rebuilds, and merged call/result rendering.
- Bash previews may need raw
partialJsonbefore arguments parse. - Preserve preview-only fields through
event-controller.ts,ui-helpers.ts, andtool-execution.ts. ToolExecutionComponent.#buildRenderContext()must work before a result exists.- Verify live streaming and rebuilt transcript paths together.
| Command | Description |
|---|---|
bun check |
Check all (TypeScript + Rust) |
bun check:ts |
Biome check + tsgo type checking |
bun check:rs |
Cargo fmt --check + clippy |
bun lint |
Lint all |
bun lint:ts |
Biome lint |
bun lint:rs |
Cargo clippy |
bun fmt |
Format all |
bun fmt:ts |
Biome format |
bun fmt:rs |
Cargo fmt |
bun fix |
Fix all (unsafe fixes + format) |
bun fix:ts |
Biome --unsafe + format-prompts |
bun fix:rs |
Clippy --fix + cargo fmt |
- Never run
bun run devorbun testunless the user instructs it. - Only run specific tests if the user instructs them, for example
bun test test/specific.test.ts. - Never commit unless the user asks.
- Do not use
tscornpx tsc; always usebun check.
- Test the externally observable contract, not the easiest internal detail.
- Every new test must defend one concrete contract: behavior, output shape, state transition, error mapping, or a regression-prone parsing boundary.
- Avoid placeholder tests, tautologies, and assertions that only prove execution.
- Prefer contract-level tests over implementation-detail tests; avoid helper wiring, incidental ordering, singleton identity, boilerplate, or passthrough assertions unless another component depends on them.
- For lifecycle or stateful code, prefer one test per invariant or transition over several tiny field-level tests.
- For error handling, trigger the real failure path and assert the surfaced contract.
- Smoke tests are acceptable only when they catch a failure mode narrower tests would miss.
- Assert exact strings, ordering, and formatting only when downstream code depends on the exact bytes.
- If a guarantee is compile-time only, enforce it with type checks or type-test coverage.
- Skip tests for tiny low-risk changes unless they affect a real contract or a regression-prone edge case.
- Prefer focused package-local verification for the changed area.
- Always read all comments on an issue.
- Use standard GitHub labels when creating issues.
- Mention the affected package in the title or description when relevant.
- Include
fixes #<number>orcloses #<number>in commit messages that close issues.
- GitHub CLI for issues and PRs.
- Use tmux for TUI interaction.
- Keep answers short and concise.
- No emojis in commits, issues, PR comments, or code.
- No fluff or cheerful filler text.
- Technical prose only; be kind but direct.
Location: packages/*/CHANGELOG.md (each package has its own)
- Under
## [Unreleased], use### Added,### Changed,### Fixed,### Removed, and### Breaking Changesas needed. - New entries always go under
## [Unreleased]. - Never modify already-released version sections.
- Released version sections are immutable.
- Attribution: internal issue-driven changes use
Fixed foo bar ([#123](https://github.com/can1357/oh-my-pi/issues/123)); external contributions useAdded feature X ([#456](https://github.com/can1357/oh-my-pi/pull/456) by [@username](https://github.com/username)).
- Update CHANGELOGs in each affected package’s
[Unreleased]section. - Run
bun run release.
The release script handles version bump, CHANGELOG finalization, commit, tag, publish, and new [Unreleased] sections.