Release notes
Nanocoder v1.27.0
What's Changed
Added Skills, a unified extension primitive that brings commands, subagents, and tools under a single ergonomic surface. Two forms over one primitive: single-file (a
.mdin.nanocoder/commands|agents|tools/, fully backwards-compatible with the existing flat dirs) and bundle (a directory under.nanocoder/skills/<name>/withskill.yamlplus optionalcommands/,agents/,tools/subdirs). Bundles are for multi-piece features that need to ship and version together: a bundle's subagent automatically gets its sibling tools, scoped tools are hidden from the global tool list (default for bundles, opt-in for single-file), and bundle commands auto-namespace (commands/status.mdin bundlegitinvokes as/git:status). New/skillsslash command lists everything loaded, and bundled members fan out into the existingCustomCommandLoader,SubagentLoader, andToolManager.registryso downstream consumers keep using their familiar registries. Closes #515.Added event-triggered skill runs via a new per-project daemon. Skill members can declare
subscribe:blocks in frontmatter (or a bundle manifest) and the daemon wakes them onfile.changedandschedule.cronevents. The daemon (nanocoder daemon <start|stop|status|logs>) runs as a long-lived background process with a lockfile, Unix-socket IPC, and installers for launchd (macOS) and systemd user units (Linux). The interactive TUI never starts event sources, so file watching and cron only run when you opt in. Triggered runs execute in a new internal headless mode (noask_user, no foreground confirmations) which supersedes the legacyschedulermode. Per-subscriptionconfirm: trueopts intoplanmode instead. Backpressure caps per-subscription concurrency and trailing-debouncesfile.changedby 500ms so chatty save loops don't pile up.Removed the legacy
source/schedule/module (runner,storage,index,types). Schedules are now expressed asschedule.cronsubscriptions on a skill member and dispatched by the daemon. The olduseSchedulerModehook,scheduler-viewcomponent, and the.nanocoder/schedules.json/.nanocoder/schedules/*.mdstorage layout are gone. The/schedulecommand has been trimmed and refocused around the new model. Closes #524.Added Custom Tools: markdown-defined, model-callable tools that sit between custom commands (prompt injection only) and MCP servers (full external process). Drop a
.mdinto.nanocoder/tools/(project) or~/.config/nanocoder/tools/(personal), declare parameters in YAML frontmatter (with JSON Schema-styletype,required,pattern,maxLength, etc.), and write a shell command body using{{ name }}and{{# section }}template placeholders. All substitutions are shell-quoted to prevent injection. Includesapproval(always/never) andread_onlyflags that participate in mode policy: plan mode requiresapproval=never && read_only=true, scheduler/headless requiresapproval=never. New/tools create <name>scaffolds a template and asks the model to help fill it in. Project tools shadow personal ones byname, and they register into the sameToolManagerregistry as built-ins and MCP tools so/tools, subagents, and mode filtering see them through a single unified registry. Closes #520.Added LLM-based context compaction as the default
/compactstrategy. The active model writes a structured markdown summary of the compressible segment (Context / Decisions / Files modified / Tools used / Open questions) using a dedicated summariser prompt, replacing the older messages with one synthetic summary while recent messages are kept verbatim. Higher fidelity than mechanical truncation at the cost of one extra round-trip. Newstrategyfield inautoCompactconfig (llmdefault,mechanicallegacy), new CLI flags (--llm,--mechanical,--strategy <name>), and automatic fallback to mechanical compression on LLM failure (network error, empty response, summary larger than original). Auto-compact uses the same strategy.Added OpenRouter request configuration via a dedicated
openrouterblock on the provider config. Forwards OpenRouter-specific request body fields (providerrouting rules,reasoning,plugins,modelsfallback list,service_tier,route,user, etc.) on every request - always-on transport/routing concerns, not gated by/tune. Provider is detected by name (case-insensitive).tune.modelParameters.reasoningEffortpopulatesopenrouter.reasoning.effortwhen unset; explicit values on the provider config always win. Closes #519.Added config lint at startup. Surfaces common misconfigurations as warnings before they cause silent failures - e.g. an
openrouterblock on a provider that is not OpenRouter, unknown fields, type mismatches. Includes a full test suite. Closes #523.Reworked OpenRouter model selection in the provider wizard. Replaced the previous list with a paginated, searchable
ModelSelectionList(12 visible items, page navigation, multi-select with running counter, select-all, error state). Makes browsing OpenRouter's hundreds of models actually usable from the wizard. Closes #516.Added unified Session Service for key generation and session infrastructure. Consolidates the various ad-hoc keying strategies scattered across commands, handlers, and tool result display into one place. Touched 50 files across handlers, commands, hooks, and the chat handler, removing duplicated key-derivation logic and shrinking
useAppStateby ~38 lines. Fully closes #229.Added the Nanocoder Battlemap - a long-form competitive comparison doc covering Nanocoder against Claude Code, OpenAI Codex CLI, Gemini CLI, Aider, OpenCode, Crush, and Pi across twelve axes (license, pricing, local-model support, MCP, extensibility, tool-calling, subagents, surface, plain mode, stars, contributors, telemetry). Honest where Nanocoder leads, honest where it does not. Closes #423.
Made code blocks copyable without ASCII artifacts. Reworked the markdown parser and
AssistantMessagerendering so fenced code blocks render as plain selectable text without surrounding box borders that previously got copied into the clipboard. Thanks to @aravindinduri.Fix: streaming OOM on long responses.
StreamingMessagewas callingwrapWithTrimmedContinuations()on the entire growing assistant message on every ~150ms flush, allocating per-line arrays proportional to the full message size. A ~37k-token (~150 KB) response would overwhelm GC and crash the 4 GB Node heap. Now slices the message to a bounded tail (MAX_LINES * textWidth * 4chars, snapped to a newline boundary) before wrapping, so per-render work is constant in the visible window rather than linear in total streamed content. Thanks to @abhishekDeshmukh74. Closes #526.Fix: performance entry buffer OOM in long sessions. React 19's
react-reconciler(dev build) callsperformance.measure/performance.markon every render, and Node's built-in fetch (undici) pushes a resource-timing entry per HTTP request. Both write to the same global buffer, which nanocoder never drained. Over long subagent-heavy sessions the buffer accumulated millions of entries until V8 thrashed in mark-compact GC and crashed withIneffective mark-compacts near heap limit. Fix installs an unref'd 30s interval that callsperformance.clearMarks()/performance.clearMeasures()in the Ink render path. Thanks to @ragini-pandey. Closes #521.Fix: auto-compact ignored the per-provider
contextWindow
…